Skip to main content

rpptx_chart/
lib.rs

1#![allow(non_camel_case_types)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5use std::io::Write;
6
7use oxml_core::OxmlError;
8use oxml_core::raw_xml::{capture_element, capture_empty_element};
9use oxml_core::xml::{local_name, matches_local_name};
10use oxml_core::xml_text::{decode_plain, resolve_entity};
11use oxml_drawing::color::{
12    ColorChoice, ColorMap, ColorMapSlot, ResolvedColor, RgbColor, ThemeColorSlot,
13    apply_color_transforms, resolve_color,
14};
15use oxml_drawing::fill::Fill;
16use oxml_drawing::order::OrderedRawChildren;
17use oxml_drawing::shape_props::{CT_ShapeProperties, ShapePropertiesError};
18use oxml_drawing::text::{CT_TextBody, CT_TextCharacterProperties, TextError};
19use oxml_drawing::theme::CT_OfficeStyleSheet;
20use oxml_layout::{
21    Color, FillRule, FontManager, GlyphRun, GroupElement, Paint, Path, PathCommand, PathElement,
22    Point, PositionedElement, Rect, Stroke, Transform,
23};
24use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
25use quick_xml::{Reader, Writer, XmlVersion};
26
27/// The transitional ChartML namespace used by PresentationML packages.
28pub const C_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
29/// The fixed ChartML prefix used by writers in this crate.
30pub const C_PREFIX: &str = "c";
31/// The DrawingML namespace written on a chart-space root.
32pub const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
33/// The office-document relationships namespace written on a chart-space root.
34pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
35const MAX_CATEGORY_ANNOTATIONS: usize = 16_384;
36const PLOT_MARGIN_LEFT: f64 = 36.0;
37const PLOT_MARGIN_RIGHT: f64 = 12.0;
38const PLOT_MARGIN_TOP: f64 = 12.0;
39const PLOT_MARGIN_BOTTOM: f64 = 28.0;
40
41/// Errors produced while reading or writing the implemented ChartML core.
42#[derive(Debug)]
43pub enum ChartError {
44    Xml(OxmlError),
45    ShapeProperties(ShapePropertiesError),
46    Text(TextError),
47    UnexpectedElement(String),
48    MissingElement(String),
49    DuplicateElement(String),
50    InvalidAttribute {
51        element: String,
52        attribute: String,
53        value: String,
54    },
55    InvalidValue {
56        element: String,
57        value: String,
58    },
59}
60
61impl fmt::Display for ChartError {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::Xml(error) => error.fmt(formatter),
65            Self::ShapeProperties(error) => error.fmt(formatter),
66            Self::Text(error) => error.fmt(formatter),
67            Self::UnexpectedElement(element) => {
68                write!(formatter, "unexpected ChartML element: {element}")
69            }
70            Self::MissingElement(element) => {
71                write!(formatter, "ChartML requires {element}")
72            }
73            Self::DuplicateElement(element) => {
74                write!(formatter, "ChartML contains duplicate {element}")
75            }
76            Self::InvalidAttribute {
77                element,
78                attribute,
79                value,
80            } => write!(
81                formatter,
82                "ChartML {element} has invalid @{attribute}: {value}"
83            ),
84            Self::InvalidValue { element, value } => {
85                write!(formatter, "ChartML {element} has invalid value: {value}")
86            }
87        }
88    }
89}
90
91impl std::error::Error for ChartError {
92    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
93        match self {
94            Self::Xml(error) => Some(error),
95            Self::ShapeProperties(error) => Some(error),
96            Self::Text(error) => Some(error),
97            _ => None,
98        }
99    }
100}
101
102impl From<OxmlError> for ChartError {
103    fn from(error: OxmlError) -> Self {
104        Self::Xml(error)
105    }
106}
107
108impl From<ShapePropertiesError> for ChartError {
109    fn from(error: ShapePropertiesError) -> Self {
110        Self::ShapeProperties(error)
111    }
112}
113
114impl From<TextError> for ChartError {
115    fn from(error: TextError) -> Self {
116        Self::Text(error)
117    }
118}
119
120pub type Result<T> = std::result::Result<T, ChartError>;
121
122/// Backend-neutral plot geometry and the plot rectangle reserved inside a chart.
123#[derive(Clone, Debug)]
124pub struct ChartGeometry {
125    pub plot_bounds: Rect,
126    pub elements: Vec<PositionedElement>,
127}
128
129#[derive(Clone, Debug)]
130struct AxisScale {
131    domain: Domain,
132    ticks: Vec<f64>,
133}
134
135#[derive(Clone, Copy, Debug)]
136struct AxisMapping {
137    domain: Domain,
138    orientation: Orientation,
139}
140
141#[derive(Clone, Copy, Debug, Default)]
142struct GeometryOptions {
143    category_orientation: Orientation,
144    value: Option<AxisMapping>,
145    scatter_x: Option<AxisMapping>,
146    scatter_y: Option<AxisMapping>,
147}
148
149#[derive(Default)]
150struct ChartAnnotations {
151    gridlines: Vec<PositionedElement>,
152    axis_lines: Vec<PositionedElement>,
153    ticks: Vec<PositionedElement>,
154    labels: Vec<PositionedElement>,
155}
156
157/// Converts one supported typed chart into a labelled backend-neutral group.
158pub fn render_chart(
159    chart: &CT_Chart,
160    bounds: Rect,
161    theme: &CT_OfficeStyleSheet,
162    color_map: &ColorMap,
163    fonts: &mut FontManager,
164) -> Result<GroupElement> {
165    let plot_bounds = geometry_plot_bounds(bounds)?;
166    let plots = chart.plot_area.plots()?;
167    let plot = plots
168        .first()
169        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
170    plot.validate()?;
171    let series_colours = resolve_series_colours(plot, theme, color_map)?;
172    let axes = chart.plot_area.axes()?;
173    let options = geometry_options(plot, &axes, chart.disp_blanks_as)?;
174    let plot_children = render_plot_geometry(
175        plot,
176        plot_bounds,
177        chart.disp_blanks_as,
178        options,
179        &series_colours,
180    )?;
181    if plot_children.is_empty() && !plot_can_render_without_marks(plot) {
182        return Err(ChartError::InvalidValue {
183            element: "c:plotArea".to_owned(),
184            value: "plot has no renderable cached data".to_owned(),
185        });
186    }
187
188    let mut chart_annotations =
189        render_axes_and_labels(plot, &axes, plot_bounds, chart.disp_blanks_as, fonts)?;
190    let mut annotations = Vec::new();
191    render_data_labels(
192        plot,
193        plot_bounds,
194        chart.disp_blanks_as,
195        options,
196        fonts,
197        &mut chart_annotations.labels,
198    )?;
199    render_legend(
200        chart,
201        plot,
202        plot_bounds,
203        fonts,
204        &series_colours,
205        &mut annotations,
206        &mut chart_annotations.labels,
207    )?;
208
209    validate_geometry_coordinates(&chart_annotations.gridlines)?;
210    validate_geometry_coordinates(&plot_children)?;
211    validate_geometry_coordinates(&chart_annotations.axis_lines)?;
212    validate_geometry_coordinates(&chart_annotations.ticks)?;
213    validate_geometry_coordinates(&annotations)?;
214    let mut children = chart_annotations.gridlines;
215    children.push(PositionedElement::Group(GroupElement {
216        transform: Transform::IDENTITY,
217        clip: Some(Path::rect(plot_bounds)),
218        opacity: 1.0,
219        effects: Vec::new(),
220        children: plot_children,
221    }));
222    children.extend(chart_annotations.axis_lines);
223    children.extend(chart_annotations.ticks);
224    children.extend(annotations);
225    children.extend(chart_annotations.labels);
226    Ok(GroupElement {
227        transform: Transform::IDENTITY,
228        clip: None,
229        opacity: 1.0,
230        effects: Vec::new(),
231        children,
232    })
233}
234
235/// Shapes the stable label used inside a visible unsupported-chart fallback.
236pub fn render_chart_placeholder(bounds: Rect, fonts: &mut FontManager) -> Result<GroupElement> {
237    if !bounds.width.is_finite()
238        || !bounds.height.is_finite()
239        || bounds.width <= 0.0
240        || bounds.height <= 0.0
241    {
242        return Err(ChartError::InvalidValue {
243            element: "chart fallback bounds".to_owned(),
244            value: "width and height must be finite and positive".to_owned(),
245        });
246    }
247    let label = shape_label(
248        fonts,
249        "Unsupported chart",
250        Point {
251            x: 4.0,
252            y: (bounds.height / 2.0).max(9.0),
253        },
254    )?;
255    Ok(GroupElement {
256        transform: Transform::IDENTITY,
257        clip: Some(Path::rect(Rect {
258            x: 0.0,
259            y: 0.0,
260            width: bounds.width,
261            height: bounds.height,
262        })),
263        opacity: 1.0,
264        effects: Vec::new(),
265        children: vec![PositionedElement::Text(label)],
266    })
267}
268
269/// Converts one supported typed chart into chart-local backend-neutral paths.
270pub fn render_geometry(
271    chart: &CT_Chart,
272    bounds: Rect,
273    theme: &CT_OfficeStyleSheet,
274    color_map: &ColorMap,
275) -> Result<ChartGeometry> {
276    let plot_bounds = geometry_plot_bounds(bounds)?;
277    let plots = chart.plot_area.plots()?;
278    let plot = plots
279        .first()
280        .ok_or_else(|| ChartError::MissingElement("c:plot".to_owned()))?;
281    plot.validate()?;
282    let series_colours = resolve_series_colours(plot, theme, color_map)?;
283    let children = render_plot_geometry(
284        plot,
285        plot_bounds,
286        chart.disp_blanks_as,
287        geometry_options(plot, &[], chart.disp_blanks_as)?,
288        &series_colours,
289    )?;
290    if children.is_empty() && !plot_can_render_without_marks(plot) {
291        return Err(ChartError::InvalidValue {
292            element: "c:plotArea".to_owned(),
293            value: "plot has no renderable cached data".to_owned(),
294        });
295    }
296    validate_geometry_coordinates(&children)?;
297    Ok(ChartGeometry {
298        plot_bounds,
299        elements: vec![PositionedElement::Group(GroupElement {
300            transform: Transform::IDENTITY,
301            clip: None,
302            opacity: 1.0,
303            effects: Vec::new(),
304            children,
305        })],
306    })
307}
308
309fn plot_can_render_without_marks(plot: &Plot) -> bool {
310    matches!(
311        plot,
312        Plot::Radar { series, .. }
313            if series
314                .iter()
315                .any(|series| !series.values.values.is_empty())
316    )
317}
318
319fn geometry_plot_bounds(bounds: Rect) -> Result<Rect> {
320    if ![bounds.x, bounds.y, bounds.width, bounds.height]
321        .into_iter()
322        .all(f64::is_finite)
323        || bounds.width <= 0.0
324        || bounds.height <= 0.0
325        || !(bounds.x + bounds.width).is_finite()
326        || !(bounds.y + bounds.height).is_finite()
327    {
328        return Err(ChartError::InvalidValue {
329            element: "chart geometry bounds".to_owned(),
330            value: format!(
331                "x={}, y={}, width={}, height={}",
332                bounds.x, bounds.y, bounds.width, bounds.height
333            ),
334        });
335    }
336    let plot_bounds = Rect {
337        x: bounds.x + PLOT_MARGIN_LEFT,
338        y: bounds.y + PLOT_MARGIN_TOP,
339        width: bounds.width - PLOT_MARGIN_LEFT - PLOT_MARGIN_RIGHT,
340        height: bounds.height - PLOT_MARGIN_TOP - PLOT_MARGIN_BOTTOM,
341    };
342    if ![
343        plot_bounds.x,
344        plot_bounds.y,
345        plot_bounds.width,
346        plot_bounds.height,
347        plot_bounds.x + plot_bounds.width,
348        plot_bounds.y + plot_bounds.height,
349    ]
350    .into_iter()
351    .all(f64::is_finite)
352        || plot_bounds.width <= 0.0
353        || plot_bounds.height <= 0.0
354    {
355        return Err(ChartError::InvalidValue {
356            element: "chart geometry bounds".to_owned(),
357            value: "chart is too small for the fixed plot margins".to_owned(),
358        });
359    }
360    Ok(plot_bounds)
361}
362
363fn geometry_options(plot: &Plot, axes: &[Axis], blanks: DispBlanksAs) -> Result<GeometryOptions> {
364    let mut options = GeometryOptions::default();
365    if let Some(category_axis) = axes.iter().find(|axis| axis.kind != AxisKind::Value) {
366        options.category_orientation = category_axis.scaling.orientation;
367    }
368
369    if let Plot::Scatter { axis_ids, .. } = plot {
370        let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
371        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[0]) {
372            options.scatter_x = Some(axis_mapping(axis, x_domain)?);
373        }
374        if let Some(axis) = axes.iter().find(|axis| axis.id == axis_ids[1]) {
375            options.scatter_y = Some(axis_mapping(axis, y_domain)?);
376        }
377    } else if let Some(domain) = plot_value_domain(plot, blanks)? {
378        options.value = Some(
379            axes.iter()
380                .find(|axis| axis.kind == AxisKind::Value)
381                .map(|axis| axis_mapping(axis, domain))
382                .transpose()?
383                .unwrap_or(AxisMapping {
384                    domain,
385                    orientation: Orientation::MinMax,
386                }),
387        );
388    }
389    Ok(options)
390}
391
392fn axis_mapping(axis: &Axis, extent: Domain) -> Result<AxisMapping> {
393    if axis.scaling.log_base.is_some() {
394        return Err(ChartError::InvalidValue {
395            element: "c:scaling/c:logBase".to_owned(),
396            value: "native chart rendering currently supports linear axes".to_owned(),
397        });
398    }
399    let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
400    Ok(AxisMapping {
401        domain: scale.domain,
402        orientation: axis.scaling.orientation,
403    })
404}
405
406fn plot_value_domain(plot: &Plot, blanks: DispBlanksAs) -> Result<Option<Domain>> {
407    let domain = match plot {
408        Plot::Bar {
409            grouping, series, ..
410        } => {
411            let logical = logical_series_values(series)?;
412            let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
413            let layers =
414                stacked_series_bounds(&logical, stacked, *grouping == BarGrouping::PercentStacked)?;
415            domain_from_layers(&layers)?
416        }
417        Plot::Line {
418            grouping, series, ..
419        } => {
420            let logical = logical_series_values(series)?;
421            if *grouping == Grouping::Standard {
422                let values = if blanks == DispBlanksAs::Zero {
423                    zero_filled_series(&logical, series_category_count(series)?)
424                        .into_iter()
425                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
426                        .collect::<Vec<_>>()
427                } else {
428                    logical
429                        .into_iter()
430                        .flat_map(|(_, points)| points.into_iter().map(|(_, value)| value))
431                        .collect::<Vec<_>>()
432                };
433                data_domain(&values, false)?
434            } else {
435                let layers =
436                    stacked_series_bounds(&logical, true, *grouping == Grouping::PercentStacked)?;
437                domain_from_layers(&layers)?
438            }
439        }
440        Plot::Area {
441            grouping, series, ..
442        } => {
443            let logical = logical_series_values(series)?;
444            let layers = stacked_series_bounds(
445                &logical,
446                *grouping != Grouping::Standard,
447                *grouping == Grouping::PercentStacked,
448            )?;
449            domain_from_layers(&layers)?
450        }
451        Plot::Radar { series, .. } => {
452            let values = series
453                .iter()
454                .flat_map(|series| series.values.values.iter().copied())
455                .collect::<Vec<_>>();
456            data_domain(&values, true)?
457        }
458        Plot::Pie { .. } | Plot::Doughnut { .. } | Plot::Scatter { .. } => return Ok(None),
459    };
460    Ok(Some(domain))
461}
462
463fn scatter_domains(plot: &Plot, blanks: DispBlanksAs) -> Result<(Domain, Domain)> {
464    let Plot::Scatter { series, .. } = plot else {
465        return Err(ChartError::UnexpectedElement("non-scatter plot".to_owned()));
466    };
467    let mut x_values = Vec::new();
468    let mut y_values = Vec::new();
469    for series in series {
470        for (_, x, y) in scatter_render_points(series, blanks)? {
471            x_values.push(x);
472            y_values.push(y);
473        }
474    }
475    Ok((
476        data_domain(&x_values, false)?,
477        data_domain(&y_values, false)?,
478    ))
479}
480
481fn render_axes_and_labels(
482    plot: &Plot,
483    axes: &[Axis],
484    bounds: Rect,
485    blanks: DispBlanksAs,
486    fonts: &mut FontManager,
487) -> Result<ChartAnnotations> {
488    if matches!(plot, Plot::Radar { .. }) {
489        return render_radar_axes_and_labels(plot, axes, bounds, blanks, fonts);
490    }
491    let mut annotations = ChartAnnotations::default();
492    let series = plot_series(plot);
493    let category_count = if series.is_empty() {
494        1
495    } else {
496        series_category_count(series)?
497    };
498    for axis in axes {
499        if axis.deleted {
500            continue;
501        }
502        axis_line(&mut annotations.axis_lines, bounds, axis.position);
503        if axis.kind == AxisKind::Value {
504            let extent = axis_extent(plot, axis, blanks)?;
505            let scale = nice_number_scale(extent, axis.scaling.minimum, axis.scaling.maximum, 6)?;
506            for tick in &scale.ticks {
507                let coordinate = axis_tick_coordinate(
508                    *tick,
509                    scale.domain,
510                    bounds,
511                    axis.position,
512                    axis.scaling.orientation,
513                )?;
514                if axis.major_gridlines.is_some() {
515                    major_gridline(
516                        &mut annotations.gridlines,
517                        bounds,
518                        axis.position,
519                        coordinate,
520                    );
521                }
522                axis_tick(
523                    &mut annotations.ticks,
524                    bounds,
525                    axis.position,
526                    coordinate,
527                    axis.major_tick_mark,
528                );
529                if axis.tick_label_position != TickLabelPosition::None {
530                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
531                    let origin = tick_label_origin(
532                        bounds,
533                        axis.position,
534                        axis.tick_label_position,
535                        coordinate,
536                    );
537                    annotations
538                        .labels
539                        .push(PositionedElement::Text(shape_label_with_properties(
540                            fonts,
541                            &text,
542                            origin,
543                            axis.tx_pr.as_ref(),
544                        )?));
545                }
546            }
547        } else {
548            let count = category_count;
549            if count > MAX_CATEGORY_ANNOTATIONS {
550                return Err(ChartError::InvalidValue {
551                    element: "c:catAx annotations".to_owned(),
552                    value: format!(
553                        "logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"
554                    ),
555                });
556            }
557            let category_labels: BTreeMap<_, _> =
558                if axis.tick_label_position != TickLabelPosition::None {
559                    series
560                        .first()
561                        .map(|series| series_categories(series, axis.number_format.as_ref()))
562                        .transpose()?
563                        .unwrap_or_default()
564                        .into_iter()
565                        .collect()
566                } else {
567                    BTreeMap::new()
568                };
569            for logical_index in 0..count {
570                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
571                let fraction = (index as f64 + 0.5) / count as f64;
572                let coordinate =
573                    if matches!(axis.position, AxisPosition::Bottom | AxisPosition::Top) {
574                        bounds.x + fraction * bounds.width
575                    } else {
576                        bounds.y + fraction * bounds.height
577                    };
578                if axis.major_gridlines.is_some() {
579                    major_gridline(
580                        &mut annotations.gridlines,
581                        bounds,
582                        axis.position,
583                        coordinate,
584                    );
585                }
586                axis_tick(
587                    &mut annotations.ticks,
588                    bounds,
589                    axis.position,
590                    coordinate,
591                    axis.major_tick_mark,
592                );
593                if axis.tick_label_position != TickLabelPosition::None
594                    && let Some(text) = category_labels.get(&logical_index)
595                {
596                    let origin = tick_label_origin(
597                        bounds,
598                        axis.position,
599                        axis.tick_label_position,
600                        coordinate,
601                    );
602                    annotations
603                        .labels
604                        .push(PositionedElement::Text(shape_label_with_properties(
605                            fonts,
606                            text,
607                            origin,
608                            axis.tx_pr.as_ref(),
609                        )?));
610                }
611            }
612        }
613    }
614    Ok(annotations)
615}
616
617fn render_radar_axes_and_labels(
618    plot: &Plot,
619    axes: &[Axis],
620    bounds: Rect,
621    blanks: DispBlanksAs,
622    fonts: &mut FontManager,
623) -> Result<ChartAnnotations> {
624    let mut annotations = ChartAnnotations::default();
625    let series = plot_series(plot);
626    let count = series_category_count(series)?;
627    if count > MAX_CATEGORY_ANNOTATIONS {
628        return Err(ChartError::InvalidValue {
629            element: "c:radarChart annotations".to_owned(),
630            value: format!("logical category count {count} exceeds {MAX_CATEGORY_ANNOTATIONS}"),
631        });
632    }
633    let center = Point {
634        x: bounds.x + bounds.width / 2.0,
635        y: bounds.y + bounds.height / 2.0,
636    };
637    let radius = bounds.width.min(bounds.height) / 2.0;
638    for axis in axes {
639        if axis.deleted {
640            continue;
641        }
642        if axis.kind == AxisKind::Value {
643            let scale = nice_number_scale(
644                axis_extent(plot, axis, blanks)?,
645                axis.scaling.minimum,
646                axis.scaling.maximum,
647                6,
648            )?;
649            annotations.axis_lines.push(stroked_segment(
650                center,
651                radial_point(center, radius, 0, count),
652                Color::BLACK,
653                1.0,
654            ));
655            for tick in &scale.ticks {
656                let distance =
657                    normalized_value_oriented(*tick, scale.domain, axis.scaling.orientation)?
658                        * radius;
659                if axis.major_gridlines.is_some() && distance > 0.0 {
660                    let points: Vec<_> = (0..count)
661                        .map(|index| radial_point(center, distance, index, count))
662                        .collect();
663                    annotations
664                        .gridlines
665                        .push(PositionedElement::Path(PathElement {
666                            path: polyline_path(&points, true),
667                            fill: None,
668                            stroke: Some(Stroke::new(
669                                Paint::Solid(Color {
670                                    r: 0.82,
671                                    g: 0.82,
672                                    b: 0.82,
673                                    a: 1.0,
674                                }),
675                                0.5,
676                            )),
677                        }));
678                }
679                radar_value_tick(
680                    &mut annotations.ticks,
681                    center,
682                    distance,
683                    axis.major_tick_mark,
684                );
685                let origin = match axis.tick_label_position {
686                    TickLabelPosition::High => Some(Point {
687                        x: center.x + radius + 4.0,
688                        y: center.y - distance + 3.0,
689                    }),
690                    TickLabelPosition::Low => Some(Point {
691                        x: center.x - radius - 30.0,
692                        y: center.y - distance + 3.0,
693                    }),
694                    TickLabelPosition::NextTo => Some(Point {
695                        x: center.x + 4.0,
696                        y: center.y - distance + 3.0,
697                    }),
698                    TickLabelPosition::None => None,
699                };
700                if let Some(origin) = origin {
701                    let text = format_axis_value(axis.number_format.as_ref(), *tick)?;
702                    annotations
703                        .labels
704                        .push(PositionedElement::Text(shape_label_with_properties(
705                            fonts,
706                            &text,
707                            origin,
708                            axis.tx_pr.as_ref(),
709                        )?));
710                }
711            }
712        } else {
713            let category_labels: BTreeMap<_, _> =
714                if axis.tick_label_position != TickLabelPosition::None {
715                    series
716                        .first()
717                        .map(|series| series_categories(series, axis.number_format.as_ref()))
718                        .transpose()?
719                        .unwrap_or_default()
720                        .into_iter()
721                        .collect()
722                } else {
723                    BTreeMap::new()
724                };
725            for logical_index in 0..count {
726                let index = oriented_category_index(logical_index, count, axis.scaling.orientation);
727                let end = radial_point(center, radius, index, count);
728                annotations
729                    .axis_lines
730                    .push(stroked_segment(center, end, Color::BLACK, 1.0));
731                radar_category_tick(
732                    &mut annotations.ticks,
733                    center,
734                    radius,
735                    index,
736                    count,
737                    axis.major_tick_mark,
738                );
739                if let Some(text) = category_labels.get(&logical_index) {
740                    let Some(origin) = radar_category_label_origin(
741                        bounds,
742                        center,
743                        radius,
744                        index,
745                        count,
746                        axis.tick_label_position,
747                    ) else {
748                        continue;
749                    };
750                    annotations
751                        .labels
752                        .push(PositionedElement::Text(shape_label_with_properties(
753                            fonts,
754                            text,
755                            origin,
756                            axis.tx_pr.as_ref(),
757                        )?));
758                }
759            }
760        }
761    }
762    Ok(annotations)
763}
764
765fn radar_category_label_origin(
766    bounds: Rect,
767    center: Point,
768    radius: f64,
769    index: usize,
770    count: usize,
771    position: TickLabelPosition,
772) -> Option<Point> {
773    let label_radius = match position {
774        TickLabelPosition::High => radius + 18.0,
775        TickLabelPosition::Low => (radius - 10.0).max(0.0),
776        TickLabelPosition::NextTo => radius + 10.0,
777        TickLabelPosition::None => return None,
778    };
779    let label = radial_point(center, label_radius, index, count);
780    let mut origin = Point {
781        x: label.x - 4.0,
782        y: label.y + 3.0,
783    };
784    if position == TickLabelPosition::High {
785        origin.x = origin.x.clamp(
786            bounds.x - PLOT_MARGIN_LEFT,
787            bounds.x + bounds.width + PLOT_MARGIN_RIGHT,
788        );
789        origin.y = origin.y.clamp(
790            bounds.y - PLOT_MARGIN_TOP,
791            bounds.y + bounds.height + PLOT_MARGIN_BOTTOM,
792        );
793    }
794    Some(origin)
795}
796
797fn radar_value_tick(
798    elements: &mut Vec<PositionedElement>,
799    center: Point,
800    distance: f64,
801    mark: TickMark,
802) {
803    let (left, right) = match mark {
804        TickMark::None => return,
805        TickMark::Inside => (4.0, 0.0),
806        TickMark::Outside => (0.0, 4.0),
807        TickMark::Cross => (2.0, 2.0),
808    };
809    let y = center.y - distance;
810    elements.push(stroked_segment(
811        Point {
812            x: center.x - left,
813            y,
814        },
815        Point {
816            x: center.x + right,
817            y,
818        },
819        Color::BLACK,
820        1.0,
821    ));
822}
823
824fn radar_category_tick(
825    elements: &mut Vec<PositionedElement>,
826    center: Point,
827    radius: f64,
828    index: usize,
829    count: usize,
830    mark: TickMark,
831) {
832    let (inside, outside) = match mark {
833        TickMark::None => return,
834        TickMark::Inside => (4.0, 0.0),
835        TickMark::Outside => (0.0, 4.0),
836        TickMark::Cross => (2.0, 2.0),
837    };
838    elements.push(stroked_segment(
839        radial_point(center, radius - inside, index, count),
840        radial_point(center, radius + outside, index, count),
841        Color::BLACK,
842        1.0,
843    ));
844}
845
846fn axis_extent(plot: &Plot, axis: &Axis, blanks: DispBlanksAs) -> Result<Domain> {
847    if let Plot::Scatter { axis_ids, .. } = plot {
848        let (x, y) = scatter_domains(plot, blanks)?;
849        return Ok(if axis.id == axis_ids[0] { x } else { y });
850    }
851    plot_value_domain(plot, blanks)?.ok_or_else(|| ChartError::InvalidValue {
852        element: "chart axis".to_owned(),
853        value: "axis-free plot supplied an axis".to_owned(),
854    })
855}
856
857fn axis_tick_coordinate(
858    value: f64,
859    domain: Domain,
860    bounds: Rect,
861    position: AxisPosition,
862    orientation: Orientation,
863) -> Result<f64> {
864    if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
865        map_x_oriented(value, domain, bounds, orientation)
866    } else {
867        map_y_oriented(value, domain, bounds, orientation)
868    }
869}
870
871fn axis_line(elements: &mut Vec<PositionedElement>, bounds: Rect, position: AxisPosition) {
872    let (start, end) = match position {
873        AxisPosition::Bottom => (
874            Point {
875                x: bounds.x,
876                y: bounds.y + bounds.height,
877            },
878            Point {
879                x: bounds.x + bounds.width,
880                y: bounds.y + bounds.height,
881            },
882        ),
883        AxisPosition::Top => (
884            Point {
885                x: bounds.x,
886                y: bounds.y,
887            },
888            Point {
889                x: bounds.x + bounds.width,
890                y: bounds.y,
891            },
892        ),
893        AxisPosition::Left => (
894            Point {
895                x: bounds.x,
896                y: bounds.y,
897            },
898            Point {
899                x: bounds.x,
900                y: bounds.y + bounds.height,
901            },
902        ),
903        AxisPosition::Right => (
904            Point {
905                x: bounds.x + bounds.width,
906                y: bounds.y,
907            },
908            Point {
909                x: bounds.x + bounds.width,
910                y: bounds.y + bounds.height,
911            },
912        ),
913    };
914    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
915}
916
917fn major_gridline(
918    elements: &mut Vec<PositionedElement>,
919    bounds: Rect,
920    position: AxisPosition,
921    coordinate: f64,
922) {
923    let (start, end) = if matches!(position, AxisPosition::Bottom | AxisPosition::Top) {
924        (
925            Point {
926                x: coordinate,
927                y: bounds.y,
928            },
929            Point {
930                x: coordinate,
931                y: bounds.y + bounds.height,
932            },
933        )
934    } else {
935        (
936            Point {
937                x: bounds.x,
938                y: coordinate,
939            },
940            Point {
941                x: bounds.x + bounds.width,
942                y: coordinate,
943            },
944        )
945    };
946    elements.push(stroked_segment(
947        start,
948        end,
949        Color {
950            r: 0.82,
951            g: 0.82,
952            b: 0.82,
953            a: 1.0,
954        },
955        0.5,
956    ));
957}
958
959fn axis_tick(
960    elements: &mut Vec<PositionedElement>,
961    bounds: Rect,
962    position: AxisPosition,
963    coordinate: f64,
964    mark: TickMark,
965) {
966    let (inside, outside) = match mark {
967        TickMark::None => return,
968        TickMark::Inside => (4.0, 0.0),
969        TickMark::Outside => (0.0, 4.0),
970        TickMark::Cross => (2.0, 2.0),
971    };
972    let (start, end) = match position {
973        AxisPosition::Bottom => (
974            Point {
975                x: coordinate,
976                y: bounds.y + bounds.height - inside,
977            },
978            Point {
979                x: coordinate,
980                y: bounds.y + bounds.height + outside,
981            },
982        ),
983        AxisPosition::Top => (
984            Point {
985                x: coordinate,
986                y: bounds.y + inside,
987            },
988            Point {
989                x: coordinate,
990                y: bounds.y - outside,
991            },
992        ),
993        AxisPosition::Left => (
994            Point {
995                x: bounds.x + inside,
996                y: coordinate,
997            },
998            Point {
999                x: bounds.x - outside,
1000                y: coordinate,
1001            },
1002        ),
1003        AxisPosition::Right => (
1004            Point {
1005                x: bounds.x + bounds.width - inside,
1006                y: coordinate,
1007            },
1008            Point {
1009                x: bounds.x + bounds.width + outside,
1010                y: coordinate,
1011            },
1012        ),
1013    };
1014    elements.push(stroked_segment(start, end, Color::BLACK, 1.0));
1015}
1016
1017fn tick_label_origin(
1018    bounds: Rect,
1019    axis_position: AxisPosition,
1020    label_position: TickLabelPosition,
1021    coordinate: f64,
1022) -> Point {
1023    let side = match label_position {
1024        TickLabelPosition::NextTo => axis_position,
1025        TickLabelPosition::High => {
1026            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1027                AxisPosition::Top
1028            } else {
1029                AxisPosition::Right
1030            }
1031        }
1032        TickLabelPosition::Low => {
1033            if matches!(axis_position, AxisPosition::Bottom | AxisPosition::Top) {
1034                AxisPosition::Bottom
1035            } else {
1036                AxisPosition::Left
1037            }
1038        }
1039        TickLabelPosition::None => axis_position,
1040    };
1041    match side {
1042        AxisPosition::Bottom => Point {
1043            x: coordinate - 4.0,
1044            y: bounds.y + bounds.height + 12.0,
1045        },
1046        AxisPosition::Top => Point {
1047            x: coordinate - 4.0,
1048            y: bounds.y - 4.0,
1049        },
1050        AxisPosition::Left => Point {
1051            x: bounds.x - 30.0,
1052            y: coordinate + 3.0,
1053        },
1054        AxisPosition::Right => Point {
1055            x: bounds.x + bounds.width + 4.0,
1056            y: coordinate + 3.0,
1057        },
1058    }
1059}
1060
1061fn format_axis_value(format: Option<&NumberFormat>, value: f64) -> Result<String> {
1062    if let Some(format) = format {
1063        format.format_value(value)
1064    } else {
1065        NumberFormat::new("General".to_owned(), false)?.format_value(value)
1066    }
1067}
1068
1069fn shape_label(fonts: &mut FontManager, text: &str, origin: Point) -> Result<GlyphRun> {
1070    shape_label_with_properties(fonts, text, origin, None)
1071}
1072
1073fn shape_label_with_properties(
1074    fonts: &mut FontManager,
1075    text: &str,
1076    origin: Point,
1077    properties: Option<&CT_TextBody>,
1078) -> Result<GlyphRun> {
1079    const FAMILY: &str = "Carlito";
1080    const SIZE: f64 = 9.0;
1081    let properties = properties.and_then(chart_text_properties);
1082    let family = properties
1083        .and_then(|properties| properties.latin.as_ref())
1084        .map(|font| font.typeface.as_str())
1085        .unwrap_or(FAMILY);
1086    let size = properties
1087        .and_then(|properties| properties.font_size)
1088        .map(|size| f64::from(size) / 100.0)
1089        .unwrap_or(SIZE);
1090    let bold = properties
1091        .and_then(|properties| properties.bold)
1092        .unwrap_or(false);
1093    let italic = properties
1094        .and_then(|properties| properties.italic)
1095        .unwrap_or(false);
1096    let font_id = fonts
1097        .resolve_font_for_text(Some(family), bold, italic, text)
1098        .map_err(|error| ChartError::InvalidValue {
1099            element: "chart label font".to_owned(),
1100            value: error.to_string(),
1101        })?;
1102    let shaped =
1103        fonts
1104            .shape_text(font_id, text, size)
1105            .map_err(|error| ChartError::InvalidValue {
1106                element: "chart label shaping".to_owned(),
1107                value: error.to_string(),
1108            })?;
1109    Ok(GlyphRun {
1110        origin,
1111        font_id,
1112        font_size: size,
1113        glyph_ids: shaped.glyph_ids,
1114        advances: shaped.advances,
1115        text: text.to_owned(),
1116        color: Color::BLACK,
1117        bold,
1118        italic,
1119        field_kind: None,
1120        footnote_id: None,
1121    })
1122}
1123
1124fn chart_text_properties(body: &CT_TextBody) -> Option<&CT_TextCharacterProperties> {
1125    body.paragraphs()
1126        .first()
1127        .and_then(|paragraph| paragraph.properties.as_ref())
1128        .and_then(|properties| properties.default_run_properties.as_ref())
1129}
1130
1131fn stroked_segment(start: Point, end: Point, color: Color, width: f64) -> PositionedElement {
1132    PositionedElement::Path(PathElement {
1133        path: Path {
1134            commands: vec![PathCommand::MoveTo(start), PathCommand::LineTo(end)],
1135            fill_rule: FillRule::NonZero,
1136        },
1137        fill: None,
1138        stroke: Some(Stroke::new(Paint::Solid(color), width)),
1139    })
1140}
1141
1142fn plot_series(plot: &Plot) -> &[Series] {
1143    match plot {
1144        Plot::Bar { series, .. }
1145        | Plot::Line { series, .. }
1146        | Plot::Pie { series, .. }
1147        | Plot::Doughnut { series, .. }
1148        | Plot::Area { series, .. }
1149        | Plot::Scatter { series, .. }
1150        | Plot::Radar { series, .. } => series,
1151    }
1152}
1153
1154fn plot_data_labels(plot: &Plot) -> Option<&CT_DLbls> {
1155    match plot {
1156        Plot::Bar { data_labels, .. }
1157        | Plot::Line { data_labels, .. }
1158        | Plot::Pie { data_labels, .. }
1159        | Plot::Doughnut { data_labels, .. }
1160        | Plot::Area { data_labels, .. }
1161        | Plot::Scatter { data_labels, .. }
1162        | Plot::Radar { data_labels, .. } => data_labels.as_ref(),
1163    }
1164}
1165
1166fn series_categories(
1167    series: &Series,
1168    number_format: Option<&NumberFormat>,
1169) -> Result<Vec<(usize, String)>> {
1170    let Some(categories) = &series.categories else {
1171        return Ok(Vec::new());
1172    };
1173    match categories {
1174        AxisData::String(data) => {
1175            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1176            indexes
1177                .into_iter()
1178                .zip(&data.values)
1179                .map(|(index, value)| {
1180                    usize::try_from(index)
1181                        .map(|index| (index, value.clone()))
1182                        .map_err(|_| ChartError::InvalidValue {
1183                            element: "c:cat/c:pt/@idx".to_owned(),
1184                            value: index.to_string(),
1185                        })
1186                })
1187                .collect()
1188        }
1189        AxisData::Numeric(data) => {
1190            let (_, indexes) = cache_layout(&data.markup, data.values.len())?;
1191            let cache_format;
1192            let format = if let Some(format) = number_format {
1193                format
1194            } else {
1195                cache_format = NumberFormat::new(data.format_code.clone(), false)?;
1196                &cache_format
1197            };
1198            indexes
1199                .into_iter()
1200                .zip(&data.values)
1201                .map(|(index, value)| {
1202                    let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1203                        element: "c:cat/c:pt/@idx".to_owned(),
1204                        value: index.to_string(),
1205                    })?;
1206                    let text = format.format_value(*value)?;
1207                    Ok((index, text))
1208                })
1209                .collect()
1210        }
1211    }
1212}
1213
1214fn series_name(series: &Series, index: usize) -> String {
1215    series
1216        .name
1217        .as_ref()
1218        .and_then(|name| name.values.first())
1219        .cloned()
1220        .unwrap_or_else(|| format!("Series {}", index + 1))
1221}
1222
1223fn render_data_labels(
1224    plot: &Plot,
1225    bounds: Rect,
1226    blanks: DispBlanksAs,
1227    options: GeometryOptions,
1228    fonts: &mut FontManager,
1229    elements: &mut Vec<PositionedElement>,
1230) -> Result<()> {
1231    let plot_labels = plot_data_labels(plot);
1232    let anchors = plot_label_anchors(plot, bounds, blanks, options)?;
1233    for (series_index, series) in plot_series(plot).iter().enumerate() {
1234        let Some(labels) = series.data_labels.as_ref().or(plot_labels) else {
1235            continue;
1236        };
1237        let overrides = point_label_overrides(labels)?;
1238        let mut categories = None;
1239        let mut bubble_sizes = None;
1240        let (_, values) = logical_numeric_values(&series.values)?;
1241        let mut percentage_total = None;
1242        for (logical_index, value) in values {
1243            let point_override = overrides.get(&logical_index);
1244            if point_override.is_some_and(|point| point.deleted == Some(true)) {
1245                continue;
1246            }
1247            let effective_labels = point_override
1248                .map(|point| point.apply_to(labels))
1249                .unwrap_or_else(|| labels.clone());
1250            let labels = &effective_labels;
1251            let Some(anchor) = anchors.get(&(series_index, logical_index)).copied() else {
1252                continue;
1253            };
1254            let mut fields = Vec::new();
1255            if labels.show_series_name {
1256                fields.push(series_name(series, series_index));
1257            }
1258            if labels.show_category_name {
1259                if categories.is_none() {
1260                    categories = Some(
1261                        series_categories(series, None)?
1262                            .into_iter()
1263                            .collect::<BTreeMap<_, _>>(),
1264                    );
1265                }
1266                if let Some(category) = categories
1267                    .as_ref()
1268                    .and_then(|categories| categories.get(&logical_index))
1269                {
1270                    fields.push(category.clone());
1271                }
1272            }
1273            if labels.show_value {
1274                fields.push(format_axis_value(labels.number_format.as_ref(), value)?);
1275            }
1276            if labels.show_percent {
1277                let total = if let Some(total) = percentage_total {
1278                    total
1279                } else {
1280                    let total = series
1281                        .values
1282                        .values
1283                        .iter()
1284                        .copied()
1285                        .filter(|value| *value > 0.0)
1286                        .try_fold(0.0, |total, value| {
1287                            checked_geometry_sum(total, value, "data-label percentage total")
1288                        })?;
1289                    percentage_total = Some(total);
1290                    total
1291                };
1292                if total > 0.0 {
1293                    let text = if let Some(format) = &labels.number_format {
1294                        format.format_value(value / total)?
1295                    } else {
1296                        NumberFormat::new("0%".to_owned(), false)?.format_value(value / total)?
1297                    };
1298                    fields.push(text);
1299                }
1300            }
1301            if labels.show_bubble_size {
1302                if bubble_sizes.is_none() {
1303                    bubble_sizes = Some(
1304                        series
1305                            .bubble_size
1306                            .as_ref()
1307                            .map(logical_numeric_values)
1308                            .transpose()?
1309                            .map(|(_, values)| values.into_iter().collect::<BTreeMap<_, _>>())
1310                            .unwrap_or_default(),
1311                    );
1312                }
1313                if let Some(value) = bubble_sizes
1314                    .as_ref()
1315                    .and_then(|sizes| sizes.get(&logical_index))
1316                {
1317                    fields.push(format_axis_value(labels.number_format.as_ref(), *value)?);
1318                }
1319            }
1320            if fields.is_empty() {
1321                continue;
1322            }
1323            let separator = labels.separator.as_deref().unwrap_or(", ");
1324            let text = fields.join(separator);
1325            let origin = anchor.origin(labels.position);
1326            elements.push(PositionedElement::Text(shape_label(fonts, &text, origin)?));
1327        }
1328    }
1329    Ok(())
1330}
1331
1332#[derive(Clone, Debug, Default)]
1333struct PointLabelOverride {
1334    index: Option<usize>,
1335    deleted: Option<bool>,
1336    number_format: Option<NumberFormat>,
1337    position: Option<DataLabelPosition>,
1338    separator: Option<String>,
1339    show_legend_key: Option<bool>,
1340    show_value: Option<bool>,
1341    show_category_name: Option<bool>,
1342    show_series_name: Option<bool>,
1343    show_percent: Option<bool>,
1344    show_bubble_size: Option<bool>,
1345}
1346
1347impl PointLabelOverride {
1348    fn apply_to(&self, labels: &CT_DLbls) -> CT_DLbls {
1349        let mut effective = labels.clone();
1350        if let Some(value) = &self.number_format {
1351            effective.number_format = Some(value.clone());
1352        }
1353        if let Some(value) = self.position {
1354            effective.position = Some(value);
1355        }
1356        if let Some(value) = &self.separator {
1357            effective.separator = Some(value.clone());
1358        }
1359        if let Some(value) = self.show_legend_key {
1360            effective.show_legend_key = value;
1361        }
1362        if let Some(value) = self.show_value {
1363            effective.show_value = value;
1364        }
1365        if let Some(value) = self.show_category_name {
1366            effective.show_category_name = value;
1367        }
1368        if let Some(value) = self.show_series_name {
1369            effective.show_series_name = value;
1370        }
1371        if let Some(value) = self.show_percent {
1372            effective.show_percent = value;
1373        }
1374        if let Some(value) = self.show_bubble_size {
1375            effective.show_bubble_size = value;
1376        }
1377        effective
1378    }
1379}
1380
1381fn point_label_overrides(labels: &CT_DLbls) -> Result<BTreeMap<usize, PointLabelOverride>> {
1382    let mut namespaces = chart_namespace_defaults();
1383    for (name, namespace) in &labels.namespace_declarations {
1384        let prefix = if name == "xmlns" {
1385            Vec::new()
1386        } else if let Some(prefix) = name.strip_prefix("xmlns:") {
1387            prefix.as_bytes().to_vec()
1388        } else {
1389            continue;
1390        };
1391        upsert_namespace_binding(&mut namespaces, prefix, namespace.clone());
1392    }
1393    let mut overrides = BTreeMap::new();
1394    for raw in labels.raw_children.at(0) {
1395        if chart_root_local(raw, &namespaces)?.as_deref() != Some(b"dLbl") {
1396            continue;
1397        }
1398        let point = parse_point_label_override(raw, &namespaces)?;
1399        let index = point
1400            .index
1401            .ok_or_else(|| ChartError::MissingElement("c:dLbl/c:idx".to_owned()))?;
1402        if overrides.insert(index, point).is_some() {
1403            return Err(ChartError::DuplicateElement(format!(
1404                "c:dLbl[c:idx={index}]"
1405            )));
1406        }
1407    }
1408    Ok(overrides)
1409}
1410
1411fn parse_point_label_override(
1412    xml: &[u8],
1413    inherited: &NamespaceBindings,
1414) -> Result<PointLabelOverride> {
1415    let mut reader = Reader::from_reader(xml);
1416    let mut buffer = Vec::new();
1417    let mut namespaces = None;
1418    let mut state = PointLabelOverride::default();
1419    let mut order = 0;
1420    loop {
1421        match reader
1422            .read_event_into(&mut buffer)
1423            .map_err(OxmlError::from)?
1424        {
1425            Event::Start(element) if namespaces.is_none() => {
1426                if !matches_local_name(element.name().as_ref(), b"dLbl")
1427                    || !element_is_in_namespace(&element, C_NS, inherited)?
1428                {
1429                    return Err(ChartError::UnexpectedElement(element_name(&element)));
1430                }
1431                namespaces = Some(chart_bindings(inherited, &element)?);
1432            }
1433            Event::Start(element) => {
1434                let bindings = namespaces
1435                    .as_ref()
1436                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1437                let name = chart_child_local(&element, bindings)?;
1438                let raw = capture_element(&mut reader, &element)?;
1439                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1440            }
1441            Event::Empty(element) => {
1442                let bindings = namespaces
1443                    .as_ref()
1444                    .ok_or_else(|| ChartError::UnexpectedElement(element_name(&element)))?;
1445                let name = chart_child_local(&element, bindings)?;
1446                let raw = capture_empty_element(&element)?;
1447                parse_point_label_child(&mut state, name.as_deref(), &raw, bindings, &mut order)?;
1448            }
1449            Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbl") => {
1450                if state.index.is_none() {
1451                    return Err(ChartError::MissingElement("c:dLbl/c:idx".to_owned()));
1452                }
1453                return Ok(state);
1454            }
1455            Event::Eof => return Err(missing_end("c:dLbl")),
1456            _ => {}
1457        }
1458        buffer.clear();
1459    }
1460}
1461
1462fn parse_point_label_child(
1463    state: &mut PointLabelOverride,
1464    name: Option<&[u8]>,
1465    raw: &[u8],
1466    namespaces: &NamespaceBindings,
1467    order: &mut usize,
1468) -> Result<()> {
1469    let Some(name) = name else {
1470        return Ok(());
1471    };
1472    let rank = match name {
1473        b"idx" => 0,
1474        b"delete" => 1,
1475        b"layout" => 2,
1476        b"tx" => 3,
1477        b"numFmt" => 4,
1478        b"spPr" => 5,
1479        b"txPr" => 6,
1480        b"dLblPos" => 7,
1481        b"showLegendKey" => 8,
1482        b"showVal" => 9,
1483        b"showCatName" => 10,
1484        b"showSerName" => 11,
1485        b"showPercent" => 12,
1486        b"showBubbleSize" => 13,
1487        b"separator" => 14,
1488        b"showLeaderLines" => 15,
1489        b"leaderLines" => 16,
1490        b"extLst" => 17,
1491        _ => return Ok(()),
1492    };
1493    if rank < *order {
1494        return Err(ChartError::InvalidValue {
1495            element: "c:dLbl".to_owned(),
1496            value: format!("c:{} is out of schema order", String::from_utf8_lossy(name)),
1497        });
1498    }
1499    *order = rank;
1500    match name {
1501        b"idx" => {
1502            let (index, _) = parse_u32_scalar(raw, "idx")?;
1503            let index = usize::try_from(index).map_err(|_| ChartError::InvalidValue {
1504                element: "c:dLbl/c:idx".to_owned(),
1505                value: index.to_string(),
1506            })?;
1507            set_once(&mut state.index, index, "c:dLbl/c:idx")?;
1508        }
1509        b"delete" => set_once(
1510            &mut state.deleted,
1511            parse_bool_value(raw, "delete")?.0,
1512            "c:dLbl/c:delete",
1513        )?,
1514        b"numFmt" => set_once(
1515            &mut state.number_format,
1516            NumberFormat::from_xml(raw)?,
1517            "c:dLbl/c:numFmt",
1518        )?,
1519        b"dLblPos" => {
1520            let (value, _) = scalar_value(raw, "dLblPos")?;
1521            let value =
1522                value.ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
1523            let position = DataLabelPosition::parse(&value)
1524                .ok_or_else(|| invalid_attribute("dLblPos", "val", value))?;
1525            set_once(&mut state.position, position, "c:dLbl/c:dLblPos")?;
1526        }
1527        b"showLegendKey" => set_once(
1528            &mut state.show_legend_key,
1529            parse_bool_value(raw, "showLegendKey")?.0,
1530            "c:dLbl/c:showLegendKey",
1531        )?,
1532        b"showVal" => set_once(
1533            &mut state.show_value,
1534            parse_bool_value(raw, "showVal")?.0,
1535            "c:dLbl/c:showVal",
1536        )?,
1537        b"showCatName" => set_once(
1538            &mut state.show_category_name,
1539            parse_bool_value(raw, "showCatName")?.0,
1540            "c:dLbl/c:showCatName",
1541        )?,
1542        b"showSerName" => set_once(
1543            &mut state.show_series_name,
1544            parse_bool_value(raw, "showSerName")?.0,
1545            "c:dLbl/c:showSerName",
1546        )?,
1547        b"showPercent" => set_once(
1548            &mut state.show_percent,
1549            parse_bool_value(raw, "showPercent")?.0,
1550            "c:dLbl/c:showPercent",
1551        )?,
1552        b"showBubbleSize" => set_once(
1553            &mut state.show_bubble_size,
1554            parse_bool_value(raw, "showBubbleSize")?.0,
1555            "c:dLbl/c:showBubbleSize",
1556        )?,
1557        b"separator" => set_once(
1558            &mut state.separator,
1559            parse_text_element(raw, b"separator", namespaces)?.0,
1560            "c:dLbl/c:separator",
1561        )?,
1562        _ => {}
1563    }
1564    Ok(())
1565}
1566
1567#[derive(Clone, Copy, Debug)]
1568struct LabelAnchor {
1569    center: Point,
1570    base: Point,
1571    end: Point,
1572    direction: Point,
1573}
1574
1575impl LabelAnchor {
1576    const fn point(center: Point) -> Self {
1577        Self {
1578            center,
1579            base: center,
1580            end: center,
1581            direction: Point { x: 0.0, y: 0.0 },
1582        }
1583    }
1584
1585    const fn segment(center: Point, base: Point, end: Point) -> Self {
1586        Self {
1587            center,
1588            base,
1589            end,
1590            direction: Point {
1591                x: end.x - base.x,
1592                y: end.y - base.y,
1593            },
1594        }
1595    }
1596
1597    const fn directed_segment(center: Point, base: Point, end: Point, direction: Point) -> Self {
1598        Self {
1599            center,
1600            base,
1601            end,
1602            direction,
1603        }
1604    }
1605
1606    fn origin(self, position: Option<DataLabelPosition>) -> Point {
1607        let mut origin = self.center;
1608        match position.unwrap_or(DataLabelPosition::BestFit) {
1609            DataLabelPosition::Bottom => origin.y += 8.0,
1610            DataLabelPosition::Top => origin.y -= 5.0,
1611            DataLabelPosition::Left => origin.x -= 8.0,
1612            DataLabelPosition::Right => origin.x += 8.0,
1613            DataLabelPosition::InsideBase => {
1614                origin = self.along_segment(self.base, 5.0, true).unwrap_or(Point {
1615                    x: origin.x,
1616                    y: origin.y + 8.0,
1617                });
1618            }
1619            DataLabelPosition::InsideEnd => {
1620                origin = self.along_segment(self.end, -5.0, true).unwrap_or(origin);
1621            }
1622            DataLabelPosition::OutsideEnd => {
1623                origin = self.along_segment(self.end, 5.0, false).unwrap_or(Point {
1624                    x: origin.x,
1625                    y: origin.y - 5.0,
1626                });
1627            }
1628            DataLabelPosition::BestFit | DataLabelPosition::Center => {}
1629        }
1630        origin
1631    }
1632
1633    fn along_segment(self, origin: Point, distance: f64, clamp_inside: bool) -> Option<Point> {
1634        let dx = self.end.x - self.base.x;
1635        let dy = self.end.y - self.base.y;
1636        let length = dx.hypot(dy);
1637        let (dx, dy, direction_length) = if length.is_finite() && length > 0.0 {
1638            (dx, dy, length)
1639        } else {
1640            let direction_length = self.direction.x.hypot(self.direction.y);
1641            if !direction_length.is_finite() || direction_length == 0.0 {
1642                return None;
1643            }
1644            (self.direction.x, self.direction.y, direction_length)
1645        };
1646        let distance = if clamp_inside {
1647            distance.clamp(-length / 2.0, length / 2.0)
1648        } else {
1649            distance
1650        };
1651        Some(Point {
1652            x: origin.x + dx / direction_length * distance,
1653            y: origin.y + dy / direction_length * distance,
1654        })
1655    }
1656}
1657
1658fn plot_label_anchors(
1659    plot: &Plot,
1660    bounds: Rect,
1661    blanks: DispBlanksAs,
1662    options: GeometryOptions,
1663) -> Result<BTreeMap<(usize, usize), LabelAnchor>> {
1664    let mut anchors = BTreeMap::new();
1665    match plot {
1666        Plot::Bar {
1667            direction,
1668            grouping,
1669            gap_width,
1670            overlap,
1671            series,
1672            ..
1673        } => {
1674            for bar in bar_rectangles(
1675                *direction, *grouping, *gap_width, *overlap, series, bounds, options,
1676            )? {
1677                if !point_in_rect(bar.end, bounds) {
1678                    continue;
1679                }
1680                let visible_base = Point {
1681                    x: bar.base.x.clamp(bounds.x, bounds.x + bounds.width),
1682                    y: bar.base.y.clamp(bounds.y, bounds.y + bounds.height),
1683                };
1684                let visible_end = Point {
1685                    x: bar.end.x.clamp(bounds.x, bounds.x + bounds.width),
1686                    y: bar.end.y.clamp(bounds.y, bounds.y + bounds.height),
1687                };
1688                anchors.insert(
1689                    (bar.series_index, bar.logical_index),
1690                    LabelAnchor::directed_segment(
1691                        Point {
1692                            x: (visible_base.x + visible_end.x) / 2.0,
1693                            y: (visible_base.y + visible_end.y) / 2.0,
1694                        },
1695                        visible_base,
1696                        visible_end,
1697                        Point {
1698                            x: bar.end.x - bar.base.x,
1699                            y: bar.end.y - bar.base.y,
1700                        },
1701                    ),
1702                );
1703            }
1704        }
1705        Plot::Line {
1706            grouping, series, ..
1707        }
1708        | Plot::Area {
1709            grouping, series, ..
1710        } => {
1711            for (series_index, points) in
1712                category_label_points(*grouping, series, bounds, blanks, options)?
1713                    .into_iter()
1714                    .enumerate()
1715            {
1716                for (logical_index, point) in points {
1717                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1718                }
1719            }
1720        }
1721        Plot::Scatter { series, .. } => {
1722            let (x_domain, y_domain) = scatter_domains(plot, blanks)?;
1723            let x_mapping = options.scatter_x.unwrap_or(AxisMapping {
1724                domain: x_domain,
1725                orientation: Orientation::MinMax,
1726            });
1727            let y_mapping = options.scatter_y.unwrap_or(AxisMapping {
1728                domain: y_domain,
1729                orientation: Orientation::MinMax,
1730            });
1731            for (series_index, series) in series.iter().enumerate() {
1732                let indexed = matched_scatter_points(series)?;
1733                let points = scatter_points(
1734                    &indexed,
1735                    x_mapping.domain,
1736                    y_mapping.domain,
1737                    bounds,
1738                    x_mapping.orientation,
1739                    y_mapping.orientation,
1740                )?;
1741                for ((logical_index, _, _), point) in indexed.into_iter().zip(points) {
1742                    anchors.insert((series_index, logical_index), LabelAnchor::point(point));
1743                }
1744            }
1745        }
1746        Plot::Radar { series, .. } => {
1747            let center = Point {
1748                x: bounds.x + bounds.width / 2.0,
1749                y: bounds.y + bounds.height / 2.0,
1750            };
1751            let category_count = series_category_count(series)?;
1752            for (series_index, points) in radar_series_points(series, bounds, options)?
1753                .into_iter()
1754                .enumerate()
1755            {
1756                for (logical_index, point) in points {
1757                    let spoke = radial_point(
1758                        center,
1759                        1.0,
1760                        oriented_category_index(
1761                            logical_index,
1762                            category_count,
1763                            options.category_orientation,
1764                        ),
1765                        category_count,
1766                    );
1767                    anchors.insert(
1768                        (series_index, logical_index),
1769                        LabelAnchor::directed_segment(
1770                            point,
1771                            center,
1772                            point,
1773                            Point {
1774                                x: spoke.x - center.x,
1775                                y: spoke.y - center.y,
1776                            },
1777                        ),
1778                    );
1779                }
1780            }
1781        }
1782        Plot::Pie {
1783            first_slice_angle,
1784            series,
1785            ..
1786        } => {
1787            for slice in pie_slices(*first_slice_angle, None, series, bounds)? {
1788                anchors.insert(
1789                    (slice.series_index, slice.logical_index),
1790                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
1791                );
1792            }
1793        }
1794        Plot::Doughnut {
1795            first_slice_angle,
1796            hole_size,
1797            series,
1798            ..
1799        } => {
1800            for slice in pie_slices(*first_slice_angle, Some(*hole_size), series, bounds)? {
1801                anchors.insert(
1802                    (slice.series_index, slice.logical_index),
1803                    LabelAnchor::segment(slice.anchor, slice.base, slice.end),
1804                );
1805            }
1806        }
1807    }
1808    anchors.retain(|_, anchor| point_in_rect(anchor.end, bounds));
1809    Ok(anchors)
1810}
1811
1812fn category_label_points(
1813    grouping: Grouping,
1814    series: &[Series],
1815    bounds: Rect,
1816    blanks: DispBlanksAs,
1817    options: GeometryOptions,
1818) -> Result<Vec<Vec<(usize, Point)>>> {
1819    validate_series_values(series)?;
1820    let logical_series = logical_series_values(series)?;
1821    let category_count = series_category_count(series)?;
1822    let calculated_series = if blanks == DispBlanksAs::Zero {
1823        zero_filled_series(&logical_series, category_count)
1824    } else {
1825        logical_series.clone()
1826    };
1827    let layers = stacked_series_bounds(
1828        &calculated_series,
1829        grouping != Grouping::Standard,
1830        grouping == Grouping::PercentStacked,
1831    )?;
1832    let domain = options
1833        .value
1834        .map(|mapping| mapping.domain)
1835        .unwrap_or(domain_from_layers(&layers)?);
1836    let value_orientation = options
1837        .value
1838        .map(|mapping| mapping.orientation)
1839        .unwrap_or(Orientation::MinMax);
1840    let mut point_series = Vec::with_capacity(layers.len());
1841    for (series_index, layer) in layers.iter().enumerate() {
1842        let present: BTreeSet<_> = logical_series[series_index]
1843            .1
1844            .iter()
1845            .map(|(index, _)| *index)
1846            .collect();
1847        let indexed: Vec<_> = layer
1848            .iter()
1849            .filter(|(index, _, _)| present.contains(index))
1850            .map(|(index, _, upper)| (*index, *upper))
1851            .collect();
1852        let points = category_points(
1853            &indexed,
1854            category_count,
1855            domain,
1856            bounds,
1857            options.category_orientation,
1858            value_orientation,
1859        )?;
1860        point_series.push(
1861            indexed
1862                .into_iter()
1863                .map(|(index, _)| index)
1864                .zip(points)
1865                .collect(),
1866        );
1867    }
1868    Ok(point_series)
1869}
1870
1871fn point_in_rect(point: Point, bounds: Rect) -> bool {
1872    const EPSILON: f64 = 1.0e-9;
1873    point.x >= bounds.x - EPSILON
1874        && point.x <= bounds.x + bounds.width + EPSILON
1875        && point.y >= bounds.y - EPSILON
1876        && point.y <= bounds.y + bounds.height + EPSILON
1877}
1878
1879fn resolve_series_colours(
1880    plot: &Plot,
1881    theme: &CT_OfficeStyleSheet,
1882    color_map: &ColorMap,
1883) -> Result<Vec<Color>> {
1884    let lookup = theme_colour_lookup(theme);
1885    plot_series(plot)
1886        .iter()
1887        .enumerate()
1888        .map(|(index, series)| {
1889            if let Some(properties) = series.sp_pr.as_ref() {
1890                if let Some(fill) = properties.fill.as_ref() {
1891                    return resolve_series_paint(fill, index, "fill", theme, color_map, &lookup);
1892                }
1893                if let Some(fill) = properties.line.as_ref().and_then(|line| line.fill.as_ref()) {
1894                    return resolve_series_paint(fill, index, "line", theme, color_map, &lookup);
1895                }
1896            }
1897            resolve_series_accent(index, theme, color_map, &lookup)
1898        })
1899        .collect()
1900}
1901
1902fn resolve_series_paint(
1903    fill: &Fill,
1904    series_index: usize,
1905    source: &str,
1906    theme: &CT_OfficeStyleSheet,
1907    color_map: &ColorMap,
1908    lookup: &[(&str, RgbColor)],
1909) -> Result<Color> {
1910    match fill {
1911        Fill::NoFill(_) => Ok(Color {
1912            r: 0.0,
1913            g: 0.0,
1914            b: 0.0,
1915            a: 0.0,
1916        }),
1917        Fill::Solid(solid) => {
1918            let choice = solid
1919                .color
1920                .as_ref()
1921                .ok_or_else(|| ChartError::InvalidValue {
1922                    element: format!("c:ser[{series_index}] direct {source} paint"),
1923                    value: "solid fill has no colour choice".to_owned(),
1924                })?;
1925            resolve_direct_chart_colour(choice, theme, color_map, lookup, || {
1926                format!("c:ser[{series_index}] direct {source} paint")
1927            })
1928        }
1929        Fill::Gradient(_) => unsupported_series_paint(series_index, source, "gradient"),
1930        Fill::Pattern(_) => unsupported_series_paint(series_index, source, "pattern"),
1931        Fill::Blip(_) => unsupported_series_paint(series_index, source, "picture"),
1932    }
1933}
1934
1935fn resolve_direct_chart_colour(
1936    choice: &ColorChoice,
1937    theme: &CT_OfficeStyleSheet,
1938    color_map: &ColorMap,
1939    lookup: &[(&str, RgbColor)],
1940    element: impl FnOnce() -> String,
1941) -> Result<Color> {
1942    let ColorChoice::Scheme {
1943        value, transforms, ..
1944    } = choice
1945    else {
1946        return resolve_chart_colour(choice, color_map, lookup, element);
1947    };
1948    let theme_slot =
1949        mapped_theme_slot(value, color_map).ok_or_else(|| ChartError::InvalidValue {
1950            element: element(),
1951            value: format!("no concrete theme slot is available for scheme colour {value}"),
1952        })?;
1953    let theme_choice = theme.theme_elements.color_scheme.color(theme_slot);
1954    let base = concrete_theme_colour(theme_choice).ok_or_else(|| ChartError::InvalidValue {
1955        element: format!("a:clrScheme/a:{}", theme_slot.as_str()),
1956        value: "selected theme slot has no concrete sRGB or system fallback colour".to_owned(),
1957    })?;
1958    let mut combined = theme_choice.transforms().to_vec();
1959    combined.extend_from_slice(transforms);
1960    Ok(resolved_layout_colour(apply_color_transforms(
1961        base, &combined,
1962    )))
1963}
1964
1965fn mapped_theme_slot(value: &str, color_map: &ColorMap) -> Option<ThemeColorSlot> {
1966    let mapped = match value {
1967        "bg1" => color_map.theme_slot(ColorMapSlot::Background1),
1968        "tx1" => color_map.theme_slot(ColorMapSlot::Text1),
1969        "bg2" => color_map.theme_slot(ColorMapSlot::Background2),
1970        "tx2" => color_map.theme_slot(ColorMapSlot::Text2),
1971        "accent1" => color_map.theme_slot(ColorMapSlot::Accent1),
1972        "accent2" => color_map.theme_slot(ColorMapSlot::Accent2),
1973        "accent3" => color_map.theme_slot(ColorMapSlot::Accent3),
1974        "accent4" => color_map.theme_slot(ColorMapSlot::Accent4),
1975        "accent5" => color_map.theme_slot(ColorMapSlot::Accent5),
1976        "accent6" => color_map.theme_slot(ColorMapSlot::Accent6),
1977        "hlink" => color_map.theme_slot(ColorMapSlot::Hyperlink),
1978        "folHlink" => color_map.theme_slot(ColorMapSlot::FollowedHyperlink),
1979        "dk1" => ThemeColorSlot::Dark1,
1980        "lt1" => ThemeColorSlot::Light1,
1981        "dk2" => ThemeColorSlot::Dark2,
1982        "lt2" => ThemeColorSlot::Light2,
1983        _ => return None,
1984    };
1985    Some(mapped)
1986}
1987
1988fn unsupported_series_paint(series_index: usize, source: &str, kind: &str) -> Result<Color> {
1989    Err(ChartError::InvalidValue {
1990        element: format!("c:ser[{series_index}] direct {source} paint"),
1991        value: format!("unsupported {kind} paint"),
1992    })
1993}
1994
1995fn resolve_series_accent(
1996    series_index: usize,
1997    theme: &CT_OfficeStyleSheet,
1998    color_map: &ColorMap,
1999    lookup: &[(&str, RgbColor)],
2000) -> Result<Color> {
2001    const ACCENTS: [ColorMapSlot; 6] = [
2002        ColorMapSlot::Accent1,
2003        ColorMapSlot::Accent2,
2004        ColorMapSlot::Accent3,
2005        ColorMapSlot::Accent4,
2006        ColorMapSlot::Accent5,
2007        ColorMapSlot::Accent6,
2008    ];
2009    let semantic_slot = ACCENTS[series_index % ACCENTS.len()];
2010    let theme_slot = color_map.theme_slot(semantic_slot);
2011    let choice = theme.theme_elements.color_scheme.color(theme_slot);
2012    resolve_chart_colour(choice, color_map, lookup, || {
2013        format!("c:ser[{series_index}] theme accent")
2014    })
2015}
2016
2017fn resolve_chart_colour(
2018    choice: &ColorChoice,
2019    color_map: &ColorMap,
2020    lookup: &[(&str, RgbColor)],
2021    element: impl FnOnce() -> String,
2022) -> Result<Color> {
2023    resolve_color(choice, color_map, lookup)
2024        .map(resolved_layout_colour)
2025        .map_err(|error| ChartError::InvalidValue {
2026            element: element(),
2027            value: error.to_string(),
2028        })
2029}
2030
2031fn theme_colour_lookup(theme: &CT_OfficeStyleSheet) -> Vec<(&'static str, RgbColor)> {
2032    theme
2033        .theme_elements
2034        .color_scheme
2035        .iter()
2036        .filter_map(|(slot, choice)| {
2037            concrete_theme_colour(choice).map(|colour| (slot.as_str(), colour))
2038        })
2039        .collect()
2040}
2041
2042fn concrete_theme_colour(choice: &ColorChoice) -> Option<RgbColor> {
2043    match choice {
2044        ColorChoice::Srgb { value, .. } => Some(*value),
2045        ColorChoice::System { last_color, .. } => *last_color,
2046        ColorChoice::Scheme { .. } | ColorChoice::Preset { .. } => None,
2047    }
2048}
2049
2050fn resolved_layout_colour(colour: ResolvedColor) -> Color {
2051    Color {
2052        r: f64::from(colour.red) / 255.0,
2053        g: f64::from(colour.green) / 255.0,
2054        b: f64::from(colour.blue) / 255.0,
2055        a: f64::from(colour.alpha) / 255.0,
2056    }
2057}
2058
2059fn render_legend(
2060    chart: &CT_Chart,
2061    plot: &Plot,
2062    bounds: Rect,
2063    fonts: &mut FontManager,
2064    series_colours: &[Color],
2065    paths: &mut Vec<PositionedElement>,
2066    labels: &mut Vec<PositionedElement>,
2067) -> Result<()> {
2068    if chart.legend.is_none() {
2069        return Ok(());
2070    }
2071    let x = bounds.x + bounds.width - 58.0;
2072    for (index, (series, colour)) in plot_series(plot).iter().zip(series_colours).enumerate() {
2073        let y = bounds.y + 6.0 + index as f64 * 13.0;
2074        paths.push(filled_path(
2075            Path::rect(Rect {
2076                x,
2077                y,
2078                width: 7.0,
2079                height: 7.0,
2080            }),
2081            *colour,
2082        ));
2083        labels.push(PositionedElement::Text(shape_label(
2084            fonts,
2085            &series_name(series, index),
2086            Point {
2087                x: x + 10.0,
2088                y: y + 7.0,
2089            },
2090        )?));
2091    }
2092    Ok(())
2093}
2094
2095fn validate_geometry_coordinates(elements: &[PositionedElement]) -> Result<()> {
2096    fn finite_point(point: Point) -> bool {
2097        point.x.is_finite() && point.y.is_finite()
2098    }
2099
2100    for element in elements {
2101        match element {
2102            PositionedElement::Path(path) => {
2103                for command in &path.path.commands {
2104                    let finite = match command {
2105                        PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
2106                            finite_point(*point)
2107                        }
2108                        PathCommand::CurveTo { c1, c2, to } => {
2109                            finite_point(*c1) && finite_point(*c2) && finite_point(*to)
2110                        }
2111                        PathCommand::Close => true,
2112                    };
2113                    if !finite {
2114                        return Err(ChartError::InvalidValue {
2115                            element: "chart geometry coordinate".to_owned(),
2116                            value: "generated path contains a nonfinite point".to_owned(),
2117                        });
2118                    }
2119                }
2120            }
2121            PositionedElement::Group(group) => validate_geometry_coordinates(&group.children)?,
2122            _ => {}
2123        }
2124    }
2125    Ok(())
2126}
2127
2128fn render_plot_geometry(
2129    plot: &Plot,
2130    bounds: Rect,
2131    blanks: DispBlanksAs,
2132    options: GeometryOptions,
2133    series_colours: &[Color],
2134) -> Result<Vec<PositionedElement>> {
2135    match plot {
2136        Plot::Bar {
2137            direction,
2138            grouping,
2139            gap_width,
2140            overlap,
2141            series,
2142            ..
2143        } => render_bar_geometry(
2144            *direction,
2145            *grouping,
2146            *gap_width,
2147            *overlap,
2148            series,
2149            bounds,
2150            options,
2151            series_colours,
2152        ),
2153        Plot::Line {
2154            grouping,
2155            marker,
2156            series,
2157            ..
2158        } => render_line_geometry(
2159            *grouping,
2160            *marker,
2161            series,
2162            bounds,
2163            blanks,
2164            options,
2165            series_colours,
2166        ),
2167        Plot::Pie {
2168            first_slice_angle,
2169            series,
2170            ..
2171        } => render_pie_geometry(*first_slice_angle, None, series, bounds, series_colours),
2172        Plot::Doughnut {
2173            first_slice_angle,
2174            hole_size,
2175            series,
2176            ..
2177        } => render_pie_geometry(
2178            *first_slice_angle,
2179            Some(*hole_size),
2180            series,
2181            bounds,
2182            series_colours,
2183        ),
2184        Plot::Area {
2185            grouping, series, ..
2186        } => render_area_geometry(*grouping, series, bounds, blanks, options, series_colours),
2187        Plot::Scatter { style, series, .. } => {
2188            render_scatter_geometry(*style, series, bounds, blanks, options, series_colours)
2189        }
2190        Plot::Radar { style, series, .. } => {
2191            render_radar_geometry(*style, series, bounds, options, series_colours)
2192        }
2193    }
2194}
2195
2196#[allow(clippy::too_many_arguments)]
2197fn render_bar_geometry(
2198    direction: BarDirection,
2199    grouping: BarGrouping,
2200    gap_width: u16,
2201    overlap: i8,
2202    series: &[Series],
2203    bounds: Rect,
2204    options: GeometryOptions,
2205    series_colours: &[Color],
2206) -> Result<Vec<PositionedElement>> {
2207    let bars = bar_rectangles(
2208        direction, grouping, gap_width, overlap, series, bounds, options,
2209    )?;
2210    bars.into_iter()
2211        .map(|bar| {
2212            Ok(filled_path(
2213                Path::rect(bar.rect),
2214                series_colour(series_colours, bar.series_index)?,
2215            ))
2216        })
2217        .collect()
2218}
2219
2220#[derive(Clone, Copy, Debug)]
2221struct BarRectangle {
2222    series_index: usize,
2223    logical_index: usize,
2224    rect: Rect,
2225    base: Point,
2226    end: Point,
2227}
2228
2229fn bar_rectangles(
2230    direction: BarDirection,
2231    grouping: BarGrouping,
2232    gap_width: u16,
2233    overlap: i8,
2234    series: &[Series],
2235    bounds: Rect,
2236    options: GeometryOptions,
2237) -> Result<Vec<BarRectangle>> {
2238    validate_series_values(series)?;
2239    let logical_series = logical_series_values(series)?;
2240    let category_count = series_category_count(series)?;
2241    let stacked = matches!(grouping, BarGrouping::Stacked | BarGrouping::PercentStacked);
2242    let percent = grouping == BarGrouping::PercentStacked;
2243    let layers = stacked_series_bounds(&logical_series, stacked, percent)?;
2244    let domain = options
2245        .value
2246        .map(|mapping| mapping.domain)
2247        .unwrap_or(domain_from_layers(&layers)?);
2248    let value_orientation = options
2249        .value
2250        .map(|mapping| mapping.orientation)
2251        .unwrap_or(Orientation::MinMax);
2252    let category_extent = if direction == BarDirection::Column {
2253        bounds.width
2254    } else {
2255        bounds.height
2256    };
2257    let slot = category_extent / category_count as f64;
2258    let cluster_extent = slot * 100.0 / (100.0 + f64::from(gap_width));
2259    let bars_per_cluster = if stacked { 1 } else { series.len() };
2260    let overlap_fraction = if stacked {
2261        0.0
2262    } else {
2263        f64::from(overlap) / 100.0
2264    };
2265    let divisor =
2266        bars_per_cluster as f64 - (bars_per_cluster.saturating_sub(1)) as f64 * overlap_fraction;
2267    let bar_extent = cluster_extent / divisor;
2268    let advance = bar_extent * (1.0 - overlap_fraction);
2269    let mut bars = Vec::new();
2270    for (series_index, layer) in layers.iter().enumerate() {
2271        for &(logical_index, start, end) in layer {
2272            let category = oriented_category_index(
2273                logical_index,
2274                category_count,
2275                options.category_orientation,
2276            );
2277            let cluster_start = category as f64 / category_count as f64 * category_extent
2278                + (slot - cluster_extent) / 2.0;
2279            let category_start = if stacked {
2280                cluster_start
2281            } else {
2282                cluster_start + series_index as f64 * advance
2283            };
2284            let (rect, base, end) = if direction == BarDirection::Column {
2285                let y0 = map_y_oriented(start, domain, bounds, value_orientation)?;
2286                let y1 = map_y_oriented(end, domain, bounds, value_orientation)?;
2287                let center_x = bounds.x + category_start + bar_extent / 2.0;
2288                (
2289                    Rect {
2290                        x: bounds.x + category_start,
2291                        y: y0.min(y1),
2292                        width: bar_extent,
2293                        height: (y1 - y0).abs(),
2294                    },
2295                    Point { x: center_x, y: y0 },
2296                    Point { x: center_x, y: y1 },
2297                )
2298            } else {
2299                let x0 = map_x_oriented(start, domain, bounds, value_orientation)?;
2300                let x1 = map_x_oriented(end, domain, bounds, value_orientation)?;
2301                let center_y = bounds.y + category_start + bar_extent / 2.0;
2302                (
2303                    Rect {
2304                        x: x0.min(x1),
2305                        y: bounds.y + category_start,
2306                        width: (x1 - x0).abs(),
2307                        height: bar_extent,
2308                    },
2309                    Point { x: x0, y: center_y },
2310                    Point { x: x1, y: center_y },
2311                )
2312            };
2313            bars.push(BarRectangle {
2314                series_index,
2315                logical_index,
2316                rect,
2317                base,
2318                end,
2319            });
2320        }
2321    }
2322    Ok(bars)
2323}
2324
2325fn render_line_geometry(
2326    grouping: Grouping,
2327    marker: bool,
2328    series: &[Series],
2329    bounds: Rect,
2330    blanks: DispBlanksAs,
2331    options: GeometryOptions,
2332    series_colours: &[Color],
2333) -> Result<Vec<PositionedElement>> {
2334    validate_series_values(series)?;
2335    let logical_series = logical_series_values(series)?;
2336    let category_count = series_category_count(series)?;
2337    let calculated_series = if blanks == DispBlanksAs::Zero {
2338        zero_filled_series(&logical_series, category_count)
2339    } else {
2340        logical_series.clone()
2341    };
2342    let stacked = grouping != Grouping::Standard;
2343    let percent = grouping == Grouping::PercentStacked;
2344    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2345    let domain = options
2346        .value
2347        .map(|mapping| mapping.domain)
2348        .unwrap_or(domain_from_layers(&layers)?);
2349    let value_orientation = options
2350        .value
2351        .map(|mapping| mapping.orientation)
2352        .unwrap_or(Orientation::MinMax);
2353    let mut elements = Vec::new();
2354    for (series_index, layer) in layers.iter().enumerate() {
2355        let colour = series_colour(series_colours, series_index)?;
2356        let indexed: Vec<_> = layer
2357            .iter()
2358            .map(|(index, _, upper)| (*index, *upper))
2359            .collect();
2360        let indexes: Vec<_> = indexed.iter().map(|(index, _)| *index).collect();
2361        for (start, end) in contiguous_ranges(&indexes, blanks) {
2362            let points = category_points(
2363                &indexed[start..end],
2364                category_count,
2365                domain,
2366                bounds,
2367                options.category_orientation,
2368                value_orientation,
2369            )?;
2370            elements.push(stroked_path(polyline_path(&points, false), colour));
2371        }
2372        if marker {
2373            let present: BTreeSet<_> = logical_series[series_index]
2374                .1
2375                .iter()
2376                .map(|(index, _)| *index)
2377                .collect();
2378            let marker_values: Vec<_> = indexed
2379                .iter()
2380                .copied()
2381                .filter(|(index, _)| present.contains(index))
2382                .collect();
2383            let marker_points = category_points(
2384                &marker_values,
2385                category_count,
2386                domain,
2387                bounds,
2388                options.category_orientation,
2389                value_orientation,
2390            )?;
2391            push_markers(&mut elements, &marker_points, colour);
2392        }
2393    }
2394    Ok(elements)
2395}
2396
2397fn render_area_geometry(
2398    grouping: Grouping,
2399    series: &[Series],
2400    bounds: Rect,
2401    blanks: DispBlanksAs,
2402    options: GeometryOptions,
2403    series_colours: &[Color],
2404) -> Result<Vec<PositionedElement>> {
2405    validate_series_values(series)?;
2406    let logical_series = logical_series_values(series)?;
2407    let category_count = series_category_count(series)?;
2408    let calculated_series = if blanks == DispBlanksAs::Zero {
2409        zero_filled_series(&logical_series, category_count)
2410    } else {
2411        logical_series
2412    };
2413    let stacked = grouping != Grouping::Standard;
2414    let percent = grouping == Grouping::PercentStacked;
2415    let layers = stacked_series_bounds(&calculated_series, stacked, percent)?;
2416    let domain = options
2417        .value
2418        .map(|mapping| mapping.domain)
2419        .unwrap_or(domain_from_layers(&layers)?);
2420    let value_orientation = options
2421        .value
2422        .map(|mapping| mapping.orientation)
2423        .unwrap_or(Orientation::MinMax);
2424    let mut elements = Vec::new();
2425    for (series_index, layer) in layers.iter().enumerate() {
2426        let colour = series_colour(series_colours, series_index)?;
2427        let indexes: Vec<_> = layer.iter().map(|(index, _, _)| *index).collect();
2428        for (start, end) in contiguous_ranges(&indexes, blanks) {
2429            let segment = &layer[start..end];
2430            let top_values: Vec<_> = segment
2431                .iter()
2432                .map(|(index, _, upper)| (*index, *upper))
2433                .collect();
2434            let lower_values: Vec<_> = segment
2435                .iter()
2436                .map(|(index, lower, _)| (*index, *lower))
2437                .collect();
2438            let top = category_points(
2439                &top_values,
2440                category_count,
2441                domain,
2442                bounds,
2443                options.category_orientation,
2444                value_orientation,
2445            )?;
2446            let mut bottom = category_points(
2447                &lower_values,
2448                category_count,
2449                domain,
2450                bounds,
2451                options.category_orientation,
2452                value_orientation,
2453            )?;
2454            bottom.reverse();
2455            let mut commands = Vec::with_capacity(top.len() + bottom.len() + 2);
2456            commands.push(PathCommand::MoveTo(top[0]));
2457            commands.extend(top.iter().skip(1).copied().map(PathCommand::LineTo));
2458            commands.extend(bottom.into_iter().map(PathCommand::LineTo));
2459            commands.push(PathCommand::Close);
2460            elements.push(filled_and_stroked_path(
2461                Path {
2462                    commands,
2463                    fill_rule: FillRule::NonZero,
2464                },
2465                colour,
2466            ));
2467        }
2468    }
2469    Ok(elements)
2470}
2471
2472fn render_scatter_geometry(
2473    style: ScatterStyle,
2474    series: &[Series],
2475    bounds: Rect,
2476    blanks: DispBlanksAs,
2477    options: GeometryOptions,
2478    series_colours: &[Color],
2479) -> Result<Vec<PositionedElement>> {
2480    validate_series_values(series)?;
2481    let mut paired_series = Vec::with_capacity(series.len());
2482    let mut marker_series = Vec::with_capacity(series.len());
2483    let mut all_x = Vec::new();
2484    let mut all_y = Vec::new();
2485    for item in series {
2486        let markers = matched_scatter_points(item)?;
2487        let paired = scatter_render_points(item, blanks)?;
2488        all_x.extend(paired.iter().map(|(_, x, _)| *x));
2489        all_y.extend(paired.iter().map(|(_, _, y)| *y));
2490        paired_series.push(paired);
2491        marker_series.push(markers);
2492    }
2493    let x_domain = options
2494        .scatter_x
2495        .map(|mapping| mapping.domain)
2496        .unwrap_or(data_domain(&all_x, false)?);
2497    let y_domain = options
2498        .scatter_y
2499        .map(|mapping| mapping.domain)
2500        .unwrap_or(data_domain(&all_y, false)?);
2501    let x_orientation = options
2502        .scatter_x
2503        .map(|mapping| mapping.orientation)
2504        .unwrap_or(Orientation::MinMax);
2505    let y_orientation = options
2506        .scatter_y
2507        .map(|mapping| mapping.orientation)
2508        .unwrap_or(Orientation::MinMax);
2509    let draw_line = matches!(
2510        style,
2511        ScatterStyle::Line
2512            | ScatterStyle::LineMarker
2513            | ScatterStyle::Smooth
2514            | ScatterStyle::SmoothMarker
2515    );
2516    let draw_marker = matches!(
2517        style,
2518        ScatterStyle::Marker | ScatterStyle::LineMarker | ScatterStyle::SmoothMarker
2519    );
2520    let mut elements = Vec::new();
2521    for (series_index, paired) in paired_series.iter().enumerate() {
2522        let colour = series_colour(series_colours, series_index)?;
2523        if draw_line {
2524            let indexes: Vec<_> = paired.iter().map(|(index, _, _)| *index).collect();
2525            for (start, end) in contiguous_ranges(&indexes, blanks) {
2526                let points = scatter_points(
2527                    &paired[start..end],
2528                    x_domain,
2529                    y_domain,
2530                    bounds,
2531                    x_orientation,
2532                    y_orientation,
2533                )?;
2534                elements.push(stroked_path(polyline_path(&points, false), colour));
2535            }
2536        }
2537        if draw_marker {
2538            let marker_points = scatter_points(
2539                &marker_series[series_index],
2540                x_domain,
2541                y_domain,
2542                bounds,
2543                x_orientation,
2544                y_orientation,
2545            )?;
2546            push_markers(&mut elements, &marker_points, colour);
2547        }
2548    }
2549    Ok(elements)
2550}
2551
2552type ScatterPoint = (usize, f64, f64);
2553
2554struct ScatterPointMaps {
2555    x_count: usize,
2556    y_count: usize,
2557    x_by_index: BTreeMap<usize, f64>,
2558    y_by_index: BTreeMap<usize, f64>,
2559}
2560
2561fn scatter_point_maps(series: &Series) -> Result<ScatterPointMaps> {
2562    let Some(AxisData::Numeric(x_values)) = &series.categories else {
2563        return Err(ChartError::InvalidValue {
2564            element: "c:xVal".to_owned(),
2565            value: "scatter series requires numeric cached x values".to_owned(),
2566        });
2567    };
2568    validate_finite(&x_values.values, "c:xVal/c:numCache")?;
2569    let (x_count, x_points) = logical_numeric_values(x_values)?;
2570    let (y_count, y_points) = logical_numeric_values(&series.values)?;
2571    Ok(ScatterPointMaps {
2572        x_count,
2573        y_count,
2574        x_by_index: x_points.into_iter().collect(),
2575        y_by_index: y_points.into_iter().collect(),
2576    })
2577}
2578
2579fn matched_scatter_points(series: &Series) -> Result<Vec<ScatterPoint>> {
2580    let points = scatter_point_maps(series)?;
2581    Ok(points
2582        .y_by_index
2583        .iter()
2584        .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2585        .collect())
2586}
2587
2588fn scatter_render_points(series: &Series, blanks: DispBlanksAs) -> Result<Vec<ScatterPoint>> {
2589    let points = scatter_point_maps(series)?;
2590    if blanks != DispBlanksAs::Zero {
2591        return Ok(points
2592            .y_by_index
2593            .iter()
2594            .filter_map(|(index, y)| points.x_by_index.get(index).map(|x| (*index, *x, *y)))
2595            .collect());
2596    }
2597    let count = points.x_count.max(points.y_count);
2598    let present = points
2599        .x_by_index
2600        .keys()
2601        .chain(points.y_by_index.keys())
2602        .copied()
2603        .collect();
2604    Ok(zero_control_indexes(count, &present)
2605        .into_iter()
2606        .map(|index| {
2607            (
2608                index,
2609                points.x_by_index.get(&index).copied().unwrap_or(0.0),
2610                points.y_by_index.get(&index).copied().unwrap_or(0.0),
2611            )
2612        })
2613        .collect())
2614}
2615
2616fn scatter_points(
2617    values: &[(usize, f64, f64)],
2618    x_domain: Domain,
2619    y_domain: Domain,
2620    bounds: Rect,
2621    x_orientation: Orientation,
2622    y_orientation: Orientation,
2623) -> Result<Vec<Point>> {
2624    values
2625        .iter()
2626        .map(|(_, x, y)| {
2627            Ok(Point {
2628                x: map_x_oriented(*x, x_domain, bounds, x_orientation)?,
2629                y: map_y_oriented(*y, y_domain, bounds, y_orientation)?,
2630            })
2631        })
2632        .collect()
2633}
2634
2635fn render_radar_geometry(
2636    style: RadarStyle,
2637    series: &[Series],
2638    bounds: Rect,
2639    options: GeometryOptions,
2640    series_colours: &[Color],
2641) -> Result<Vec<PositionedElement>> {
2642    let point_series = radar_series_points(series, bounds, options)?;
2643    let mut elements = Vec::new();
2644    for (series_index, points) in point_series.iter().enumerate() {
2645        let colour = series_colour(series_colours, series_index)?;
2646        let plain_points: Vec<_> = points.iter().map(|(_, point)| *point).collect();
2647        if plain_points.is_empty() {
2648            continue;
2649        }
2650        let path = polyline_path(&plain_points, true);
2651        if style == RadarStyle::Filled {
2652            elements.push(filled_and_stroked_path(path, colour));
2653        } else {
2654            elements.push(stroked_path(path, colour));
2655        }
2656        if style == RadarStyle::Marker {
2657            push_markers(&mut elements, &plain_points, colour);
2658        }
2659    }
2660    Ok(elements)
2661}
2662
2663fn radar_series_points(
2664    series: &[Series],
2665    bounds: Rect,
2666    options: GeometryOptions,
2667) -> Result<Vec<Vec<(usize, Point)>>> {
2668    validate_series_values(series)?;
2669    let logical_series = logical_series_values(series)?;
2670    let category_count = series_category_count(series)?;
2671    let values = logical_series
2672        .iter()
2673        .flat_map(|(_, points)| points.iter().map(|(_, value)| *value))
2674        .collect::<Vec<_>>();
2675    let mapping = options.value.unwrap_or(AxisMapping {
2676        domain: data_domain(&values, true)?,
2677        orientation: Orientation::MinMax,
2678    });
2679    let center = Point {
2680        x: bounds.x + bounds.width / 2.0,
2681        y: bounds.y + bounds.height / 2.0,
2682    };
2683    let radius = bounds.width.min(bounds.height) / 2.0;
2684    let mut point_series = Vec::with_capacity(logical_series.len());
2685    for (_, logical) in &logical_series {
2686        let mut points = Vec::with_capacity(logical.len());
2687        for (category, value) in logical {
2688            if *value < mapping.domain.min || *value > mapping.domain.max {
2689                continue;
2690            }
2691            points.push((
2692                *category,
2693                radial_point(
2694                    center,
2695                    normalized_value_oriented(*value, mapping.domain, mapping.orientation)?
2696                        * radius,
2697                    oriented_category_index(
2698                        *category,
2699                        category_count,
2700                        options.category_orientation,
2701                    ),
2702                    category_count,
2703                ),
2704            ));
2705        }
2706        point_series.push(points);
2707    }
2708    Ok(point_series)
2709}
2710
2711fn render_pie_geometry(
2712    first_slice_angle: u16,
2713    hole_size: Option<u8>,
2714    series: &[Series],
2715    bounds: Rect,
2716    series_colours: &[Color],
2717) -> Result<Vec<PositionedElement>> {
2718    pie_slices(first_slice_angle, hole_size, series, bounds)?
2719        .into_iter()
2720        .map(|slice| {
2721            Ok(filled_path(
2722                slice.path,
2723                series_colour(series_colours, slice.series_index)?,
2724            ))
2725        })
2726        .collect()
2727}
2728
2729struct PieSliceGeometry {
2730    series_index: usize,
2731    logical_index: usize,
2732    path: Path,
2733    anchor: Point,
2734    base: Point,
2735    end: Point,
2736}
2737
2738fn pie_slices(
2739    first_slice_angle: u16,
2740    hole_size: Option<u8>,
2741    series: &[Series],
2742    bounds: Rect,
2743) -> Result<Vec<PieSliceGeometry>> {
2744    validate_series_values(series)?;
2745    let center = Point {
2746        x: bounds.x + bounds.width / 2.0,
2747        y: bounds.y + bounds.height / 2.0,
2748    };
2749    let radius = bounds.width.min(bounds.height) / 2.0;
2750    let mut elements = Vec::new();
2751    for (series_index, item) in series.iter().enumerate() {
2752        let (_, logical) = logical_numeric_values(&item.values)?;
2753        let total = logical
2754            .iter()
2755            .map(|(_, value)| *value)
2756            .filter(|value| *value > 0.0)
2757            .try_fold(0.0, |total, value| {
2758                checked_geometry_sum(total, value, "pie value total")
2759            })?;
2760        if total <= 0.0 {
2761            continue;
2762        }
2763        let mut angle = f64::from(first_slice_angle).to_radians() - std::f64::consts::FRAC_PI_2;
2764        for (logical_index, value) in logical {
2765            if value <= 0.0 {
2766                continue;
2767            }
2768            let next = angle + value / total * std::f64::consts::TAU;
2769            let path = if let Some(hole_size) = hole_size {
2770                doughnut_wedge(
2771                    center,
2772                    radius,
2773                    radius * f64::from(hole_size) / 100.0,
2774                    angle,
2775                    next,
2776                )
2777            } else {
2778                pie_wedge(center, radius, angle, next)
2779            };
2780            let inner_radius = hole_size
2781                .map(|hole| radius * f64::from(hole) / 100.0)
2782                .unwrap_or(0.0);
2783            let anchor_radius = hole_size
2784                .map(|_| (radius + inner_radius) / 2.0)
2785                .unwrap_or(radius * 2.0 / 3.0);
2786            let midpoint = (angle + next) / 2.0;
2787            elements.push(PieSliceGeometry {
2788                series_index,
2789                logical_index,
2790                path,
2791                anchor: circle_point(center, anchor_radius, midpoint),
2792                base: circle_point(center, inner_radius, midpoint),
2793                end: circle_point(center, radius, midpoint),
2794            });
2795            angle = next;
2796        }
2797    }
2798    Ok(elements)
2799}
2800
2801fn validate_series_values(series: &[Series]) -> Result<()> {
2802    if series.is_empty() {
2803        return Err(ChartError::MissingElement("c:ser".to_owned()));
2804    }
2805    for item in series {
2806        validate_finite(&item.values.values, "c:val/c:numCache")?;
2807    }
2808    Ok(())
2809}
2810
2811fn validate_finite(values: &[f64], element: &str) -> Result<()> {
2812    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
2813        return Err(ChartError::InvalidValue {
2814            element: element.to_owned(),
2815            value: value.to_string(),
2816        });
2817    }
2818    Ok(())
2819}
2820
2821type IndexedValues = Vec<(usize, f64)>;
2822type LogicalSeries = (usize, IndexedValues);
2823type GeometryLayer = Vec<(usize, f64, f64)>;
2824
2825fn zero_control_indexes(count: usize, present: &BTreeSet<usize>) -> BTreeSet<usize> {
2826    let mut controls = present.clone();
2827    if count == 0 {
2828        return controls;
2829    }
2830    controls.insert(0);
2831    controls.insert(count - 1);
2832    for index in present {
2833        if *index > 0 {
2834            controls.insert(*index - 1);
2835        }
2836        if index + 1 < count {
2837            controls.insert(index + 1);
2838        }
2839    }
2840    controls
2841}
2842
2843fn zero_filled_series(series: &[LogicalSeries], count: usize) -> Vec<LogicalSeries> {
2844    let present = series
2845        .iter()
2846        .flat_map(|(_, points)| points.iter().map(|(index, _)| *index))
2847        .collect();
2848    let controls = zero_control_indexes(count, &present);
2849    series
2850        .iter()
2851        .map(|(declared, points)| {
2852            let values: BTreeMap<_, _> = points.iter().copied().collect();
2853            let filled = controls
2854                .iter()
2855                .map(|index| (*index, values.get(index).copied().unwrap_or(0.0)))
2856                .collect();
2857            (*declared, filled)
2858        })
2859        .collect()
2860}
2861
2862fn contiguous_ranges(indexes: &[usize], blanks: DispBlanksAs) -> Vec<(usize, usize)> {
2863    if indexes.is_empty() {
2864        return Vec::new();
2865    }
2866    if blanks != DispBlanksAs::Gap {
2867        return vec![(0, indexes.len())];
2868    }
2869    let mut ranges = Vec::new();
2870    let mut start = 0usize;
2871    for position in 1..indexes.len() {
2872        if indexes[position - 1].checked_add(1) != Some(indexes[position]) {
2873            ranges.push((start, position));
2874            start = position;
2875        }
2876    }
2877    ranges.push((start, indexes.len()));
2878    ranges
2879}
2880
2881fn logical_numeric_values(data: &NumericData) -> Result<LogicalSeries> {
2882    validate_finite(&data.values, "c:numCache")?;
2883    let (declared, indexes) = cache_layout(&data.markup, data.values.len())?;
2884    let declared = usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
2885        element: "c:ptCount".to_owned(),
2886        value: declared.to_string(),
2887    })?;
2888    let points = indexes
2889        .into_iter()
2890        .zip(data.values.iter().copied())
2891        .map(|(index, value)| {
2892            usize::try_from(index)
2893                .map(|index| (index, value))
2894                .map_err(|_| ChartError::InvalidValue {
2895                    element: "c:pt/@idx".to_owned(),
2896                    value: index.to_string(),
2897                })
2898        })
2899        .collect::<Result<Vec<_>>>()?;
2900    Ok((declared, points))
2901}
2902
2903fn logical_series_values(series: &[Series]) -> Result<Vec<LogicalSeries>> {
2904    series
2905        .iter()
2906        .map(|item| logical_numeric_values(&item.values))
2907        .collect()
2908}
2909
2910fn reference_logical_count(markup: &ReferenceMarkup, value_count: usize) -> Result<usize> {
2911    let (declared, _) = cache_layout(markup, value_count)?;
2912    usize::try_from(declared).map_err(|_| ChartError::InvalidValue {
2913        element: "c:ptCount".to_owned(),
2914        value: declared.to_string(),
2915    })
2916}
2917
2918fn series_category_count(series: &[Series]) -> Result<usize> {
2919    let mut count = 0usize;
2920    for item in series {
2921        count = count.max(reference_logical_count(
2922            &item.values.markup,
2923            item.values.values.len(),
2924        )?);
2925        if let Some(categories) = &item.categories {
2926            let category_count = match categories {
2927                AxisData::String(data) => reference_logical_count(&data.markup, data.values.len())?,
2928                AxisData::Numeric(data) => {
2929                    reference_logical_count(&data.markup, data.values.len())?
2930                }
2931            };
2932            count = count.max(category_count);
2933        }
2934    }
2935    if count == 0 {
2936        return Err(ChartError::InvalidValue {
2937            element: "c:ser".to_owned(),
2938            value: "cached series contain no points".to_owned(),
2939        });
2940    }
2941    Ok(count)
2942}
2943
2944fn stacked_series_bounds(
2945    series: &[LogicalSeries],
2946    stacked: bool,
2947    percent: bool,
2948) -> Result<Vec<GeometryLayer>> {
2949    let mut positive_totals = BTreeMap::<usize, f64>::new();
2950    let mut negative_totals = BTreeMap::<usize, f64>::new();
2951    if percent {
2952        for (_, points) in series {
2953            for &(index, value) in points {
2954                let totals = if value >= 0.0 {
2955                    &mut positive_totals
2956                } else {
2957                    &mut negative_totals
2958                };
2959                let total = totals.entry(index).or_default();
2960                *total = checked_geometry_sum(*total, value.abs(), "stacked percentage total")?;
2961            }
2962        }
2963    }
2964    let mut positive = BTreeMap::<usize, f64>::new();
2965    let mut negative = BTreeMap::<usize, f64>::new();
2966    let mut layers = Vec::with_capacity(series.len());
2967    for (_, points) in series {
2968        let mut layer = Vec::with_capacity(points.len());
2969        for &(index, original) in points {
2970            let total = if original >= 0.0 {
2971                positive_totals.get(&index).copied().unwrap_or(0.0)
2972            } else {
2973                negative_totals.get(&index).copied().unwrap_or(0.0)
2974            };
2975            let value = if percent && total != 0.0 {
2976                original / total
2977            } else {
2978                original
2979            };
2980            if stacked {
2981                let accumulators = if value >= 0.0 {
2982                    &mut positive
2983                } else {
2984                    &mut negative
2985                };
2986                let accumulator = accumulators.entry(index).or_default();
2987                let lower = *accumulator;
2988                *accumulator = checked_geometry_sum(*accumulator, value, "stacked value total")?;
2989                layer.push((index, lower, *accumulator));
2990            } else {
2991                layer.push((index, 0.0, value));
2992            }
2993        }
2994        layers.push(layer);
2995    }
2996    Ok(layers)
2997}
2998
2999fn checked_geometry_sum(current: f64, value: f64, context: &str) -> Result<f64> {
3000    let sum = current + value;
3001    if sum.is_finite() {
3002        Ok(sum)
3003    } else {
3004        Err(ChartError::InvalidValue {
3005            element: "chart geometry aggregate".to_owned(),
3006            value: format!("{context} is nonfinite"),
3007        })
3008    }
3009}
3010
3011#[derive(Clone, Copy, Debug, PartialEq)]
3012struct Domain {
3013    min: f64,
3014    max: f64,
3015}
3016
3017fn nice_number_scale(
3018    extent: Domain,
3019    pinned_minimum: Option<f64>,
3020    pinned_maximum: Option<f64>,
3021    target_tick_count: usize,
3022) -> Result<AxisScale> {
3023    if target_tick_count < 2
3024        || !extent.min.is_finite()
3025        || !extent.max.is_finite()
3026        || pinned_minimum.is_some_and(|value| !value.is_finite())
3027        || pinned_maximum.is_some_and(|value| !value.is_finite())
3028    {
3029        return Err(ChartError::InvalidValue {
3030            element: "chart axis scale".to_owned(),
3031            value: "scale bounds and target tick count must be finite and valid".to_owned(),
3032        });
3033    }
3034
3035    let mut data_min = extent.min.min(extent.max);
3036    let mut data_max = extent.min.max(extent.max);
3037    if data_min == data_max {
3038        let expanded = expand_constant_domain(data_min)?;
3039        data_min = expanded.min;
3040        data_max = expanded.max;
3041    }
3042    let requested_min = match (pinned_minimum, pinned_maximum) {
3043        (Some(minimum), _) => minimum,
3044        (None, Some(maximum)) if data_min >= maximum => {
3045            extend_bound(maximum, stable_positive_step(data_min, data_max, 3)?, false)?
3046        }
3047        (None, _) => data_min,
3048    };
3049    let requested_max = match (pinned_minimum, pinned_maximum) {
3050        (_, Some(maximum)) => maximum,
3051        (Some(minimum), None) if data_max <= minimum => {
3052            extend_bound(minimum, stable_positive_step(data_min, data_max, 3)?, true)?
3053        }
3054        (_, None) => data_max,
3055    };
3056    if requested_min >= requested_max {
3057        return Err(ChartError::InvalidValue {
3058            element: "c:scaling".to_owned(),
3059            value: format!("minimum {requested_min} is not less than maximum {requested_max}"),
3060        });
3061    }
3062
3063    let raw_step = stable_positive_step(requested_min, requested_max, target_tick_count)?;
3064    let step = nice_step(raw_step)?;
3065    let rounded_minimum = (requested_min / step).floor() * step;
3066    let rounded_maximum = (requested_max / step).ceil() * step;
3067    let lower_fallback = pinned_minimum.is_none() && !rounded_minimum.is_finite();
3068    let upper_fallback = pinned_maximum.is_none() && !rounded_maximum.is_finite();
3069    let minimum = pinned_minimum.unwrap_or(if rounded_minimum.is_finite() {
3070        rounded_minimum
3071    } else {
3072        requested_min
3073    });
3074    let maximum = pinned_maximum.unwrap_or(if rounded_maximum.is_finite() {
3075        rounded_maximum
3076    } else {
3077        requested_max
3078    });
3079    if !minimum.is_finite() || !maximum.is_finite() || minimum >= maximum {
3080        return Err(ChartError::InvalidValue {
3081            element: "chart axis scale".to_owned(),
3082            value: "nice-number bounds are nonfinite or empty".to_owned(),
3083        });
3084    }
3085
3086    let first = (minimum / step).ceil();
3087    let last = (maximum / step).floor();
3088    let tick_count = if last >= first {
3089        (last - first).min(1024.0) as usize + 1
3090    } else {
3091        0
3092    };
3093    let mut ticks = Vec::with_capacity(tick_count);
3094    for index in 0..tick_count {
3095        let tick = (first + index as f64) * step;
3096        if tick.is_finite() && tick >= minimum && tick <= maximum {
3097            ticks.push(clean_tick(tick, step));
3098        }
3099    }
3100    if lower_fallback && ticks.first().is_none_or(|tick| *tick > minimum) {
3101        ticks.insert(0, minimum);
3102    }
3103    if upper_fallback && ticks.last().is_none_or(|tick| *tick < maximum) {
3104        ticks.push(maximum);
3105    }
3106    if ticks.len() < 2 {
3107        ticks.extend([minimum, maximum]);
3108        ticks.sort_by(f64::total_cmp);
3109    }
3110    ticks.dedup_by(|left, right| *left == *right);
3111    if ticks.len() < 2 || !ticks.windows(2).all(|pair| pair[0] < pair[1]) {
3112        return Err(ChartError::InvalidValue {
3113            element: "chart axis scale".to_owned(),
3114            value: "scale cannot produce two increasing finite ticks".to_owned(),
3115        });
3116    }
3117    Ok(AxisScale {
3118        domain: Domain {
3119            min: minimum,
3120            max: maximum,
3121        },
3122        ticks,
3123    })
3124}
3125
3126const MIN_POSITIVE_SUBNORMAL: f64 = f64::from_bits(1);
3127
3128fn stable_positive_step(minimum: f64, maximum: f64, target_tick_count: usize) -> Result<f64> {
3129    let scale = minimum.abs().max(maximum.abs());
3130    let scale = if scale == 0.0 {
3131        MIN_POSITIVE_SUBNORMAL
3132    } else {
3133        scale
3134    };
3135    let normalized =
3136        (maximum / scale - minimum / scale) / target_tick_count.saturating_sub(1) as f64;
3137    let step = normalized * scale;
3138    if normalized.is_finite() && normalized > 0.0 && step.is_finite() {
3139        Ok(step.max(MIN_POSITIVE_SUBNORMAL))
3140    } else {
3141        Err(ChartError::InvalidValue {
3142            element: "chart axis scale".to_owned(),
3143            value: "scale step is nonfinite or empty".to_owned(),
3144        })
3145    }
3146}
3147
3148fn nice_step(raw_step: f64) -> Result<f64> {
3149    if !raw_step.is_finite() || raw_step <= 0.0 {
3150        return Err(ChartError::InvalidValue {
3151            element: "chart axis scale".to_owned(),
3152            value: format!("invalid raw tick step {raw_step}"),
3153        });
3154    }
3155    let magnitude = 10.0_f64
3156        .powf(raw_step.log10().floor())
3157        .max(MIN_POSITIVE_SUBNORMAL);
3158    let fraction = raw_step / magnitude;
3159    let multiplier = if fraction <= 1.0 {
3160        1.0
3161    } else if fraction <= 2.0 {
3162        2.0
3163    } else if fraction <= 5.0 {
3164        5.0
3165    } else {
3166        10.0
3167    };
3168    let step = (multiplier * magnitude).max(MIN_POSITIVE_SUBNORMAL);
3169    if step.is_finite() && step > 0.0 {
3170        Ok(step)
3171    } else {
3172        Err(ChartError::InvalidValue {
3173            element: "chart axis scale".to_owned(),
3174            value: "nice-number step is nonfinite".to_owned(),
3175        })
3176    }
3177}
3178
3179fn expand_constant_domain(value: f64) -> Result<Domain> {
3180    if value == 0.0 {
3181        return Ok(Domain {
3182            min: -1.0,
3183            max: 1.0,
3184        });
3185    }
3186    let padding = (value.abs() * 0.5).max(MIN_POSITIVE_SUBNORMAL);
3187    let minimum = value - padding;
3188    let maximum = value + padding;
3189    if minimum.is_finite() && maximum.is_finite() && minimum < maximum {
3190        return Ok(Domain {
3191            min: minimum,
3192            max: maximum,
3193        });
3194    }
3195    let domain = if value.is_sign_positive() {
3196        Domain {
3197            min: 0.0,
3198            max: value,
3199        }
3200    } else {
3201        Domain {
3202            min: value,
3203            max: 0.0,
3204        }
3205    };
3206    if domain.min < domain.max {
3207        Ok(domain)
3208    } else {
3209        Err(ChartError::InvalidValue {
3210            element: "chart axis scale".to_owned(),
3211            value: format!("constant value {value} has no finite renderable extent"),
3212        })
3213    }
3214}
3215
3216fn extend_bound(bound: f64, span: f64, positive: bool) -> Result<f64> {
3217    let candidate = if positive { bound + span } else { bound - span };
3218    let extended = if candidate.is_finite()
3219        && if positive {
3220            candidate > bound
3221        } else {
3222            candidate < bound
3223        } {
3224        candidate
3225    } else if positive {
3226        bound.next_up()
3227    } else {
3228        bound.next_down()
3229    };
3230    if extended.is_finite()
3231        && if positive {
3232            extended > bound
3233        } else {
3234            extended < bound
3235        }
3236    {
3237        Ok(extended)
3238    } else {
3239        Err(ChartError::InvalidValue {
3240            element: "c:scaling".to_owned(),
3241            value: format!("one-sided bound {bound} leaves no finite automatic extent"),
3242        })
3243    }
3244}
3245
3246fn clean_tick(value: f64, step: f64) -> f64 {
3247    if value.abs() <= step.abs() * 1.0e-12 {
3248        0.0
3249    } else {
3250        value
3251    }
3252}
3253
3254fn domain_from_layers(layers: &[GeometryLayer]) -> Result<Domain> {
3255    let values = layers
3256        .iter()
3257        .flatten()
3258        .flat_map(|(_, lower, upper)| [*lower, *upper]);
3259    let mut min = 0.0_f64;
3260    let mut max = 0.0_f64;
3261    for value in values {
3262        if !value.is_finite() {
3263            return Err(ChartError::InvalidValue {
3264                element: "chart geometry domain".to_owned(),
3265                value: "aggregate produced a nonfinite value".to_owned(),
3266            });
3267        }
3268        min = min.min(value);
3269        max = max.max(value);
3270    }
3271    if min == max {
3272        max = 1.0;
3273    }
3274    Ok(Domain { min, max })
3275}
3276
3277fn data_domain(values: &[f64], include_zero: bool) -> Result<Domain> {
3278    validate_finite(values, "chart geometry cache")?;
3279    let Some(first) = values.first().copied() else {
3280        return Err(ChartError::InvalidValue {
3281            element: "c:numCache".to_owned(),
3282            value: "cached series contain no points".to_owned(),
3283        });
3284    };
3285    let mut min = first;
3286    let mut max = first;
3287    for value in values.iter().copied() {
3288        min = min.min(value);
3289        max = max.max(value);
3290    }
3291    if include_zero {
3292        min = min.min(0.0);
3293        max = max.max(0.0);
3294    }
3295    if min == max {
3296        if !include_zero {
3297            return expand_constant_domain(min);
3298        }
3299        if min > 0.0 {
3300            min = 0.0;
3301        } else if max < 0.0 {
3302            max = 0.0;
3303        } else {
3304            max = 1.0;
3305        }
3306    }
3307    Ok(Domain { min, max })
3308}
3309
3310fn normalized_value(value: f64, domain: Domain) -> Result<f64> {
3311    let scale = value
3312        .abs()
3313        .max(domain.min.abs())
3314        .max(domain.max.abs())
3315        .max(1.0);
3316    let value = value / scale;
3317    let min = domain.min / scale;
3318    let max = domain.max / scale;
3319    let range = max - min;
3320    let normalized = (value - min) / range;
3321    if !range.is_finite() || range <= 0.0 || !normalized.is_finite() {
3322        return Err(ChartError::InvalidValue {
3323            element: "chart geometry domain".to_owned(),
3324            value: format!("cannot map finite domain {} to {}", domain.min, domain.max),
3325        });
3326    }
3327    Ok(normalized)
3328}
3329
3330fn normalized_value_oriented(value: f64, domain: Domain, orientation: Orientation) -> Result<f64> {
3331    let normalized = normalized_value(value, domain)?;
3332    Ok(if orientation == Orientation::MaxMin {
3333        1.0 - normalized
3334    } else {
3335        normalized
3336    })
3337}
3338
3339fn map_x_oriented(
3340    value: f64,
3341    domain: Domain,
3342    bounds: Rect,
3343    orientation: Orientation,
3344) -> Result<f64> {
3345    let coordinate =
3346        bounds.x + normalized_value_oriented(value, domain, orientation)? * bounds.width;
3347    if coordinate.is_finite() {
3348        Ok(coordinate)
3349    } else {
3350        Err(ChartError::InvalidValue {
3351            element: "chart geometry coordinate".to_owned(),
3352            value: "x coordinate is nonfinite".to_owned(),
3353        })
3354    }
3355}
3356
3357fn map_y_oriented(
3358    value: f64,
3359    domain: Domain,
3360    bounds: Rect,
3361    orientation: Orientation,
3362) -> Result<f64> {
3363    let coordinate = bounds.y + bounds.height
3364        - normalized_value_oriented(value, domain, orientation)? * bounds.height;
3365    if coordinate.is_finite() {
3366        Ok(coordinate)
3367    } else {
3368        Err(ChartError::InvalidValue {
3369            element: "chart geometry coordinate".to_owned(),
3370            value: "y coordinate is nonfinite".to_owned(),
3371        })
3372    }
3373}
3374
3375fn oriented_category_index(index: usize, count: usize, orientation: Orientation) -> usize {
3376    if orientation == Orientation::MaxMin && index < count {
3377        count - index - 1
3378    } else {
3379        index
3380    }
3381}
3382
3383fn category_points(
3384    values: &[(usize, f64)],
3385    count: usize,
3386    domain: Domain,
3387    bounds: Rect,
3388    category_orientation: Orientation,
3389    value_orientation: Orientation,
3390) -> Result<Vec<Point>> {
3391    values
3392        .iter()
3393        .map(|(index, value)| {
3394            let index = oriented_category_index(*index, count, category_orientation);
3395            let x = bounds.x + (index as f64 + 0.5) / count as f64 * bounds.width;
3396            let y = map_y_oriented(*value, domain, bounds, value_orientation)?;
3397            if !x.is_finite() {
3398                return Err(ChartError::InvalidValue {
3399                    element: "chart geometry coordinate".to_owned(),
3400                    value: "category x coordinate is nonfinite".to_owned(),
3401                });
3402            }
3403            Ok(Point { x, y })
3404        })
3405        .collect()
3406}
3407
3408fn polyline_path(points: &[Point], close: bool) -> Path {
3409    let mut commands = Vec::with_capacity(points.len() + usize::from(close));
3410    if let Some(first) = points.first() {
3411        commands.push(PathCommand::MoveTo(*first));
3412        commands.extend(points.iter().skip(1).copied().map(PathCommand::LineTo));
3413        if close {
3414            commands.push(PathCommand::Close);
3415        }
3416    }
3417    Path {
3418        commands,
3419        fill_rule: FillRule::NonZero,
3420    }
3421}
3422
3423fn pie_wedge(center: Point, radius: f64, start: f64, end: f64) -> Path {
3424    let first = circle_point(center, radius, start);
3425    let mut commands = vec![PathCommand::MoveTo(center), PathCommand::LineTo(first)];
3426    append_arc(&mut commands, center, radius, start, end);
3427    commands.push(PathCommand::Close);
3428    Path {
3429        commands,
3430        fill_rule: FillRule::NonZero,
3431    }
3432}
3433
3434fn doughnut_wedge(
3435    center: Point,
3436    outer_radius: f64,
3437    inner_radius: f64,
3438    start: f64,
3439    end: f64,
3440) -> Path {
3441    let outer_start = circle_point(center, outer_radius, start);
3442    let inner_end = circle_point(center, inner_radius, end);
3443    let mut commands = vec![PathCommand::MoveTo(outer_start)];
3444    append_arc(&mut commands, center, outer_radius, start, end);
3445    commands.push(PathCommand::LineTo(inner_end));
3446    append_arc(&mut commands, center, inner_radius, end, start);
3447    commands.push(PathCommand::Close);
3448    Path {
3449        commands,
3450        fill_rule: FillRule::NonZero,
3451    }
3452}
3453
3454fn append_arc(commands: &mut Vec<PathCommand>, center: Point, radius: f64, start: f64, end: f64) {
3455    let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
3456        .ceil()
3457        .max(1.0) as usize;
3458    let sweep = (end - start) / segments as f64;
3459    for segment in 0..segments {
3460        let angle = start + segment as f64 * sweep;
3461        let next = angle + sweep;
3462        let factor = 4.0 / 3.0 * (sweep / 4.0).tan();
3463        let from = circle_point(center, radius, angle);
3464        let to = circle_point(center, radius, next);
3465        commands.push(PathCommand::CurveTo {
3466            c1: Point {
3467                x: from.x - factor * radius * angle.sin(),
3468                y: from.y + factor * radius * angle.cos(),
3469            },
3470            c2: Point {
3471                x: to.x + factor * radius * next.sin(),
3472                y: to.y - factor * radius * next.cos(),
3473            },
3474            to,
3475        });
3476    }
3477}
3478
3479fn circle_point(center: Point, radius: f64, angle: f64) -> Point {
3480    Point {
3481        x: center.x + radius * angle.cos(),
3482        y: center.y + radius * angle.sin(),
3483    }
3484}
3485
3486fn radial_point(center: Point, radius: f64, index: usize, count: usize) -> Point {
3487    circle_point(
3488        center,
3489        radius,
3490        index as f64 * std::f64::consts::TAU / count as f64 - std::f64::consts::FRAC_PI_2,
3491    )
3492}
3493
3494fn marker_path(center: Point) -> Path {
3495    const RADIUS: f64 = 2.5;
3496    const KAPPA: f64 = 0.552_284_749_830_793_6;
3497    let control = RADIUS * KAPPA;
3498    Path {
3499        commands: vec![
3500            PathCommand::MoveTo(Point {
3501                x: center.x + RADIUS,
3502                y: center.y,
3503            }),
3504            PathCommand::CurveTo {
3505                c1: Point {
3506                    x: center.x + RADIUS,
3507                    y: center.y + control,
3508                },
3509                c2: Point {
3510                    x: center.x + control,
3511                    y: center.y + RADIUS,
3512                },
3513                to: Point {
3514                    x: center.x,
3515                    y: center.y + RADIUS,
3516                },
3517            },
3518            PathCommand::CurveTo {
3519                c1: Point {
3520                    x: center.x - control,
3521                    y: center.y + RADIUS,
3522                },
3523                c2: Point {
3524                    x: center.x - RADIUS,
3525                    y: center.y + control,
3526                },
3527                to: Point {
3528                    x: center.x - RADIUS,
3529                    y: center.y,
3530                },
3531            },
3532            PathCommand::CurveTo {
3533                c1: Point {
3534                    x: center.x - RADIUS,
3535                    y: center.y - control,
3536                },
3537                c2: Point {
3538                    x: center.x - control,
3539                    y: center.y - RADIUS,
3540                },
3541                to: Point {
3542                    x: center.x,
3543                    y: center.y - RADIUS,
3544                },
3545            },
3546            PathCommand::CurveTo {
3547                c1: Point {
3548                    x: center.x + control,
3549                    y: center.y - RADIUS,
3550                },
3551                c2: Point {
3552                    x: center.x + RADIUS,
3553                    y: center.y - control,
3554                },
3555                to: Point {
3556                    x: center.x + RADIUS,
3557                    y: center.y,
3558                },
3559            },
3560            PathCommand::Close,
3561        ],
3562        fill_rule: FillRule::NonZero,
3563    }
3564}
3565
3566fn push_markers(elements: &mut Vec<PositionedElement>, points: &[Point], colour: Color) {
3567    for point in points {
3568        elements.push(filled_path(marker_path(*point), colour));
3569    }
3570}
3571
3572fn series_colour(series_colours: &[Color], index: usize) -> Result<Color> {
3573    series_colours
3574        .get(index)
3575        .copied()
3576        .ok_or_else(|| ChartError::InvalidValue {
3577            element: "chart series palette".to_owned(),
3578            value: format!("missing resolved colour for series {index}"),
3579        })
3580}
3581
3582fn filled_path(path: Path, color: Color) -> PositionedElement {
3583    PositionedElement::Path(PathElement {
3584        path,
3585        fill: Some(Paint::Solid(color)),
3586        stroke: None,
3587    })
3588}
3589
3590fn stroked_path(path: Path, color: Color) -> PositionedElement {
3591    PositionedElement::Path(PathElement {
3592        path,
3593        fill: None,
3594        stroke: Some(Stroke::new(Paint::Solid(color), 1.5)),
3595    })
3596}
3597
3598fn filled_and_stroked_path(path: Path, color: Color) -> PositionedElement {
3599    PositionedElement::Path(PathElement {
3600        path,
3601        fill: Some(Paint::Solid(Color {
3602            a: color.a * 0.55,
3603            ..color
3604        })),
3605        stroke: Some(Stroke::new(Paint::Solid(color), 1.0)),
3606    })
3607}
3608
3609type NamespaceBindings = Vec<(Vec<u8>, String)>;
3610type XmlAttributes = Vec<(String, String)>;
3611type RootAttributes = (XmlAttributes, XmlAttributes);
3612
3613#[derive(Clone, Debug, Default, Eq, PartialEq)]
3614struct ScalarMarkup {
3615    raw_attributes: XmlAttributes,
3616    raw_content: Vec<Vec<u8>>,
3617}
3618
3619#[derive(Clone, Debug, Default, Eq, PartialEq)]
3620struct TextMarkup {
3621    raw_attributes: XmlAttributes,
3622    raw_children: OrderedRawChildren,
3623}
3624
3625#[derive(Clone, Debug, Default, Eq, PartialEq)]
3626struct PointMarkup {
3627    raw_attributes: XmlAttributes,
3628    raw_children: OrderedRawChildren,
3629    value: TextMarkup,
3630}
3631
3632#[derive(Clone, Debug, Default, Eq, PartialEq)]
3633struct ReferenceMarkup {
3634    raw_attributes: XmlAttributes,
3635    raw_children: OrderedRawChildren,
3636    formula: TextMarkup,
3637    cache_attributes: XmlAttributes,
3638    cache_children: OrderedRawChildren,
3639    format_code: Option<TextMarkup>,
3640    point_count: ScalarMarkup,
3641    declared_point_count: Option<u32>,
3642    point_indexes: Vec<u32>,
3643    points: Vec<PointMarkup>,
3644}
3645
3646#[derive(Clone, Debug, Default, Eq, PartialEq)]
3647struct WrapperMarkup {
3648    raw_attributes: XmlAttributes,
3649    raw_children: OrderedRawChildren,
3650}
3651
3652/// A formula-backed string cache whose count and point indexes are derived.
3653#[derive(Clone, Debug, PartialEq)]
3654pub struct StringRef {
3655    pub formula: String,
3656    pub values: Vec<String>,
3657    markup: ReferenceMarkup,
3658}
3659
3660impl StringRef {
3661    pub fn new(formula: String, values: Vec<String>) -> Result<Self> {
3662        let reference = Self {
3663            formula,
3664            values,
3665            markup: ReferenceMarkup::default(),
3666        };
3667        reference.validate()?;
3668        Ok(reference)
3669    }
3670
3671    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3672        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3673    }
3674
3675    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3676        let parsed = parse_reference(xml, b"strRef", b"strCache", false, inherited)?;
3677        let values = parsed.values;
3678        let reference = Self {
3679            formula: parsed.formula,
3680            values,
3681            markup: parsed.markup,
3682        };
3683        reference.validate()?;
3684        Ok(reference)
3685    }
3686
3687    pub fn to_xml(&self) -> Result<Vec<u8>> {
3688        self.validate()?;
3689        let mut writer = Writer::new(Vec::new());
3690        self.write_xml(&mut writer, true)?;
3691        Ok(writer.into_inner())
3692    }
3693
3694    fn validate(&self) -> Result<()> {
3695        validate_formula(&self.formula, "c:strRef/c:f")?;
3696        for value in &self.values {
3697            validate_xml_text(value, "c:strCache/c:pt/c:v")?;
3698        }
3699        validate_point_count(self.values.len(), "c:strCache")
3700    }
3701
3702    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
3703        self.validate()?;
3704        let mut start = BytesStart::new("c:strRef");
3705        if standalone {
3706            start.push_attribute(("xmlns:c", C_NS));
3707        }
3708        push_attributes(&mut start, &self.markup.raw_attributes);
3709        writer
3710            .write_event(Event::Start(start))
3711            .map_err(OxmlError::from)?;
3712        emit_raw(writer, self.markup.raw_children.at(0))?;
3713        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
3714        emit_raw(writer, self.markup.raw_children.at(1))?;
3715        write_string_cache(writer, &self.values, &self.markup)?;
3716        emit_raw(writer, self.markup.raw_children.at(2))?;
3717        writer
3718            .write_event(Event::End(BytesEnd::new("c:strRef")))
3719            .map_err(OxmlError::from)?;
3720        Ok(())
3721    }
3722}
3723
3724/// A formula-backed numeric cache whose metadata and points are derived.
3725#[derive(Clone, Debug, PartialEq)]
3726pub struct NumericData {
3727    pub formula: String,
3728    pub format_code: String,
3729    pub values: Vec<f64>,
3730    markup: ReferenceMarkup,
3731}
3732
3733impl NumericData {
3734    pub fn new(formula: String, format_code: String, values: Vec<f64>) -> Result<Self> {
3735        validate_formula(&formula, "c:numRef/c:f")?;
3736        validate_format_code(&format_code)?;
3737        validate_numeric_values(&values)?;
3738        validate_point_count(values.len(), "c:numCache")?;
3739        Ok(Self {
3740            formula,
3741            format_code,
3742            values,
3743            markup: ReferenceMarkup::default(),
3744        })
3745    }
3746
3747    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3748        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3749    }
3750
3751    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3752        let parsed = parse_reference(xml, b"numRef", b"numCache", true, inherited)?;
3753        let format_code = parsed
3754            .format_code
3755            .ok_or_else(|| ChartError::MissingElement("c:formatCode".to_owned()))?;
3756        let mut values = Vec::with_capacity(parsed.values.len());
3757        for value in parsed.values {
3758            let number = value.parse::<f64>().map_err(|_| ChartError::InvalidValue {
3759                element: "c:v".to_owned(),
3760                value: value.clone(),
3761            })?;
3762            if !number.is_finite() {
3763                return Err(ChartError::InvalidValue {
3764                    element: "c:v".to_owned(),
3765                    value,
3766                });
3767            }
3768            values.push(number);
3769        }
3770        let data = Self {
3771            formula: parsed.formula,
3772            format_code,
3773            values,
3774            markup: parsed.markup,
3775        };
3776        data.validate()?;
3777        Ok(data)
3778    }
3779
3780    pub fn to_xml(&self) -> Result<Vec<u8>> {
3781        self.validate()?;
3782        let mut writer = Writer::new(Vec::new());
3783        self.write_xml(&mut writer, true)?;
3784        Ok(writer.into_inner())
3785    }
3786
3787    fn validate(&self) -> Result<()> {
3788        validate_formula(&self.formula, "c:numRef/c:f")?;
3789        validate_format_code(&self.format_code)?;
3790        validate_numeric_values(&self.values)?;
3791        validate_point_count(self.values.len(), "c:numCache")
3792    }
3793
3794    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
3795        self.validate()?;
3796        let mut start = BytesStart::new("c:numRef");
3797        if standalone {
3798            start.push_attribute(("xmlns:c", C_NS));
3799        }
3800        push_attributes(&mut start, &self.markup.raw_attributes);
3801        writer
3802            .write_event(Event::Start(start))
3803            .map_err(OxmlError::from)?;
3804        emit_raw(writer, self.markup.raw_children.at(0))?;
3805        write_text(writer, "c:f", &self.formula, &self.markup.formula)?;
3806        emit_raw(writer, self.markup.raw_children.at(1))?;
3807        write_numeric_cache(writer, self, &self.markup)?;
3808        emit_raw(writer, self.markup.raw_children.at(2))?;
3809        writer
3810            .write_event(Event::End(BytesEnd::new("c:numRef")))
3811            .map_err(OxmlError::from)?;
3812        Ok(())
3813    }
3814}
3815
3816#[derive(Clone, Debug, PartialEq)]
3817pub enum AxisData {
3818    String(StringRef),
3819    Numeric(NumericData),
3820}
3821
3822/// The common formula-backed payload of one ChartML series.
3823#[derive(Clone, Debug, PartialEq)]
3824pub struct Series {
3825    pub index: u32,
3826    pub order: u32,
3827    pub name: Option<StringRef>,
3828    pub categories: Option<AxisData>,
3829    pub values: NumericData,
3830    pub bubble_size: Option<NumericData>,
3831    pub sp_pr: Option<CT_ShapeProperties>,
3832    pub data_labels: Option<CT_DLbls>,
3833    index_markup: ScalarMarkup,
3834    order_markup: ScalarMarkup,
3835    name_markup: Option<WrapperMarkup>,
3836    categories_markup: Option<WrapperMarkup>,
3837    values_markup: WrapperMarkup,
3838    bubble_size_markup: Option<WrapperMarkup>,
3839    opaque_name: bool,
3840    opaque_categories: bool,
3841    opaque_bubble_size: bool,
3842    uses_scatter_wrappers: bool,
3843    parsed_from_xml: bool,
3844    raw_attributes: XmlAttributes,
3845    namespace_declarations: XmlAttributes,
3846    raw_children: OrderedRawChildren,
3847}
3848
3849impl Series {
3850    pub fn new(index: u32, order: u32, values: NumericData) -> Self {
3851        Self {
3852            index,
3853            order,
3854            name: None,
3855            categories: None,
3856            values,
3857            bubble_size: None,
3858            sp_pr: None,
3859            data_labels: None,
3860            index_markup: ScalarMarkup::default(),
3861            order_markup: ScalarMarkup::default(),
3862            name_markup: None,
3863            categories_markup: None,
3864            values_markup: WrapperMarkup::default(),
3865            bubble_size_markup: None,
3866            opaque_name: false,
3867            opaque_categories: false,
3868            opaque_bubble_size: false,
3869            uses_scatter_wrappers: false,
3870            parsed_from_xml: false,
3871            raw_attributes: Vec::new(),
3872            namespace_declarations: Vec::new(),
3873            raw_children: OrderedRawChildren::default(),
3874        }
3875    }
3876
3877    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3878        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
3879    }
3880
3881    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
3882        let mut reader = Reader::from_reader(xml);
3883        let mut buffer = Vec::new();
3884        loop {
3885            match reader
3886                .read_event_into(&mut buffer)
3887                .map_err(OxmlError::from)?
3888            {
3889                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
3890                    chart_root_prefix(&element)?;
3891                    if !element_is_in_namespace(&element, C_NS, inherited)? {
3892                        return Err(ChartError::UnexpectedElement(element_name(&element)));
3893                    }
3894                    return Self::from_element(&mut reader, &element, inherited);
3895                }
3896                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
3897                    return Err(ChartError::MissingElement("c:idx".to_owned()));
3898                }
3899                Event::Start(element) | Event::Empty(element) => {
3900                    return Err(ChartError::UnexpectedElement(element_name(&element)));
3901                }
3902                Event::Eof => return Err(ChartError::MissingElement("c:ser".to_owned())),
3903                _ => {}
3904            }
3905            buffer.clear();
3906        }
3907    }
3908
3909    fn from_element(
3910        reader: &mut Reader<&[u8]>,
3911        start: &BytesStart<'_>,
3912        inherited: &NamespaceBindings,
3913    ) -> Result<Self> {
3914        reject_conflicting_prefix(start, b"a", A_NS)?;
3915        let namespaces = chart_bindings(inherited, start)?;
3916        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
3917        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
3918        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
3919        let (raw_attributes, _) =
3920            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
3921        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
3922        let mut state = SeriesParseState::default();
3923        let mut buffer = Vec::new();
3924        loop {
3925            match reader
3926                .read_event_into(&mut buffer)
3927                .map_err(OxmlError::from)?
3928            {
3929                Event::Start(element) => {
3930                    let name = chart_child_local(&element, &namespaces)?;
3931                    let raw = capture_element(reader, &element)?;
3932                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
3933                }
3934                Event::Empty(element) => {
3935                    let name = chart_child_local(&element, &namespaces)?;
3936                    let raw = capture_empty_element(&element)?;
3937                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
3938                }
3939                event @ (Event::Text(_)
3940                | Event::CData(_)
3941                | Event::Comment(_)
3942                | Event::PI(_)
3943                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
3944                Event::End(element) if matches_local_name(element.name().as_ref(), b"ser") => {
3945                    break;
3946                }
3947                Event::Eof => return Err(missing_end("c:ser")),
3948                _ => {}
3949            }
3950            buffer.clear();
3951        }
3952        state.finish(raw_attributes, namespace_declarations)
3953    }
3954
3955    pub fn to_xml(&self) -> Result<Vec<u8>> {
3956        self.to_xml_for_plot(self.uses_scatter_wrappers)
3957    }
3958
3959    fn to_xml_for_plot(&self, scatter: bool) -> Result<Vec<u8>> {
3960        if self.opaque_name && self.name.is_some() {
3961            return Err(ChartError::DuplicateElement("c:tx".to_owned()));
3962        }
3963        if self.opaque_categories && self.categories.is_some() {
3964            return Err(ChartError::DuplicateElement("c:cat".to_owned()));
3965        }
3966        if self.opaque_bubble_size && self.bubble_size.is_some() {
3967            return Err(ChartError::DuplicateElement("c:bubbleSize".to_owned()));
3968        }
3969        self.values.validate()?;
3970        if let Some(name) = &self.name {
3971            name.validate()?;
3972        }
3973        if let Some(categories) = &self.categories {
3974            if scatter && !matches!(categories, AxisData::Numeric(_)) {
3975                return Err(ChartError::InvalidValue {
3976                    element: "c:xVal".to_owned(),
3977                    value: "scatter x values must use a numeric cache".to_owned(),
3978                });
3979            }
3980            match categories {
3981                AxisData::String(reference) => reference.validate()?,
3982                AxisData::Numeric(reference) => reference.validate()?,
3983            }
3984        }
3985        if scatter && self.categories.is_none() {
3986            return Err(ChartError::MissingElement("c:xVal".to_owned()));
3987        }
3988        if let Some(size) = &self.bubble_size {
3989            size.validate()?;
3990        }
3991        if let Some(labels) = &self.data_labels {
3992            labels.validate()?;
3993        }
3994        let mut writer = Writer::new(Vec::new());
3995        let mut start = BytesStart::new("c:ser");
3996        start.push_attribute(("xmlns:c", C_NS));
3997        start.push_attribute(("xmlns:a", A_NS));
3998        start.push_attribute(("xmlns:r", R_NS));
3999        push_attributes(&mut start, &self.namespace_declarations);
4000        push_attributes(&mut start, &self.raw_attributes);
4001        writer
4002            .write_event(Event::Start(start))
4003            .map_err(OxmlError::from)?;
4004        emit_raw(&mut writer, self.raw_children.at(0))?;
4005        write_scalar(
4006            &mut writer,
4007            "c:idx",
4008            &self.index.to_string(),
4009            Some(&self.index_markup),
4010        )?;
4011        emit_raw(&mut writer, self.raw_children.at(1))?;
4012        write_scalar(
4013            &mut writer,
4014            "c:order",
4015            &self.order.to_string(),
4016            Some(&self.order_markup),
4017        )?;
4018        emit_raw(&mut writer, self.raw_children.at(2))?;
4019        if let Some(name) = &self.name {
4020            write_wrapper_start(
4021                &mut writer,
4022                "c:tx",
4023                self.name_markup
4024                    .as_ref()
4025                    .unwrap_or(&WrapperMarkup::default()),
4026            )?;
4027            name.write_xml(&mut writer, false)?;
4028            write_wrapper_end(
4029                &mut writer,
4030                "c:tx",
4031                self.name_markup
4032                    .as_ref()
4033                    .unwrap_or(&WrapperMarkup::default()),
4034            )?;
4035        }
4036        emit_raw(&mut writer, self.raw_children.at(3))?;
4037        if let Some(properties) = &self.sp_pr {
4038            properties.write_xml_as(&mut writer, "c:spPr")?;
4039        }
4040        emit_raw(&mut writer, self.raw_children.at(4))?;
4041        if let Some(labels) = &self.data_labels {
4042            labels.write_xml(&mut writer, false)?;
4043        }
4044        emit_raw(&mut writer, self.raw_children.at(5))?;
4045        if let Some(categories) = &self.categories {
4046            let default_markup = WrapperMarkup::default();
4047            let markup = self.categories_markup.as_ref().unwrap_or(&default_markup);
4048            write_wrapper_start(
4049                &mut writer,
4050                if scatter { "c:xVal" } else { "c:cat" },
4051                markup,
4052            )?;
4053            match categories {
4054                AxisData::String(reference) => reference.write_xml(&mut writer, false)?,
4055                AxisData::Numeric(reference) => reference.write_xml(&mut writer, false)?,
4056            }
4057            write_wrapper_end(
4058                &mut writer,
4059                if scatter { "c:xVal" } else { "c:cat" },
4060                markup,
4061            )?;
4062        }
4063        emit_raw(&mut writer, self.raw_children.at(6))?;
4064        write_wrapper_start(
4065            &mut writer,
4066            if scatter { "c:yVal" } else { "c:val" },
4067            &self.values_markup,
4068        )?;
4069        self.values.write_xml(&mut writer, false)?;
4070        write_wrapper_end(
4071            &mut writer,
4072            if scatter { "c:yVal" } else { "c:val" },
4073            &self.values_markup,
4074        )?;
4075        emit_raw(&mut writer, self.raw_children.at(7))?;
4076        if let Some(size) = &self.bubble_size {
4077            let default_markup = WrapperMarkup::default();
4078            let markup = self.bubble_size_markup.as_ref().unwrap_or(&default_markup);
4079            write_wrapper_start(&mut writer, "c:bubbleSize", markup)?;
4080            size.write_xml(&mut writer, false)?;
4081            write_wrapper_end(&mut writer, "c:bubbleSize", markup)?;
4082        }
4083        emit_raw(&mut writer, self.raw_children.at(8))?;
4084        writer
4085            .write_event(Event::End(BytesEnd::new("c:ser")))
4086            .map_err(OxmlError::from)?;
4087        Ok(writer.into_inner())
4088    }
4089
4090    pub fn raw_children(&self) -> &OrderedRawChildren {
4091        &self.raw_children
4092    }
4093}
4094
4095#[derive(Default)]
4096struct SeriesParseState {
4097    index: Option<(u32, ScalarMarkup)>,
4098    order: Option<(u32, ScalarMarkup)>,
4099    name: Option<(StringRef, WrapperMarkup)>,
4100    categories: Option<(AxisData, WrapperMarkup)>,
4101    values: Option<(NumericData, WrapperMarkup)>,
4102    bubble_size: Option<(NumericData, WrapperMarkup)>,
4103    data_labels: Option<CT_DLbls>,
4104    name_seen: bool,
4105    categories_seen: bool,
4106    bubble_size_seen: bool,
4107    opaque_name: bool,
4108    opaque_categories: bool,
4109    opaque_bubble_size: bool,
4110    saw_standard_wrapper: bool,
4111    saw_scatter_wrapper: bool,
4112    sp_pr: Option<CT_ShapeProperties>,
4113    raw_children: OrderedRawChildren,
4114    boundary: usize,
4115}
4116
4117impl SeriesParseState {
4118    fn capture_event(&mut self, raw: Vec<u8>) {
4119        self.raw_children.push(self.boundary, raw);
4120    }
4121
4122    fn parse_child(
4123        &mut self,
4124        name: &[u8],
4125        raw: Vec<u8>,
4126        namespaces: &NamespaceBindings,
4127    ) -> Result<()> {
4128        match name {
4129            b"idx" => {
4130                set_once(&mut self.index, parse_u32_scalar(&raw, "idx")?, "c:idx")?;
4131                self.boundary = self.boundary.max(1);
4132            }
4133            b"order" => {
4134                set_once(&mut self.order, parse_u32_scalar(&raw, "order")?, "c:order")?;
4135                self.boundary = self.boundary.max(2);
4136            }
4137            b"tx" => {
4138                mark_once(&mut self.name_seen, "c:tx")?;
4139                let parsed = parse_wrapper(&raw, b"tx", &[b"strRef"], namespaces)?;
4140                if let Some((_, reference)) = parsed.choice {
4141                    self.name = Some((
4142                        StringRef::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4143                        parsed.markup,
4144                    ));
4145                } else {
4146                    self.opaque_name = true;
4147                    self.raw_children.push(2, raw);
4148                }
4149                self.boundary = self.boundary.max(3);
4150            }
4151            b"spPr" => {
4152                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
4153                let properties = CT_ShapeProperties::from_xml(&raw)?;
4154                let mut writer = Writer::new(Vec::new());
4155                properties.write_xml_as(&mut writer, "c:spPr")?;
4156                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
4157                set_once(&mut self.sp_pr, properties, "c:spPr")?;
4158                self.boundary = self.boundary.max(4);
4159            }
4160            b"dLbls" => {
4161                set_once(
4162                    &mut self.data_labels,
4163                    CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
4164                    "c:dLbls",
4165                )?;
4166                self.boundary = self.boundary.max(5);
4167            }
4168            b"cat" => {
4169                self.saw_standard_wrapper = true;
4170                mark_once(&mut self.categories_seen, "c:cat")?;
4171                let parsed = parse_wrapper(&raw, b"cat", &[b"strRef", b"numRef"], namespaces)?;
4172                if let Some((choice, reference)) = parsed.choice {
4173                    let categories = if choice == b"strRef" {
4174                        AxisData::String(StringRef::from_xml_with_namespaces(
4175                            &reference,
4176                            &parsed.namespaces,
4177                        )?)
4178                    } else {
4179                        AxisData::Numeric(NumericData::from_xml_with_namespaces(
4180                            &reference,
4181                            &parsed.namespaces,
4182                        )?)
4183                    };
4184                    self.categories = Some((categories, parsed.markup));
4185                } else {
4186                    self.opaque_categories = true;
4187                    self.raw_children.push(5, raw);
4188                }
4189                self.boundary = self.boundary.max(6);
4190            }
4191            b"xVal" => {
4192                self.saw_scatter_wrapper = true;
4193                mark_once(&mut self.categories_seen, "c:xVal")?;
4194                let parsed = parse_wrapper(&raw, b"xVal", &[b"numRef"], namespaces)?;
4195                let (_, reference) = parsed
4196                    .choice
4197                    .ok_or_else(|| ChartError::MissingElement("c:xVal/c:numRef".to_owned()))?;
4198                self.categories = Some((
4199                    AxisData::Numeric(NumericData::from_xml_with_namespaces(
4200                        &reference,
4201                        &parsed.namespaces,
4202                    )?),
4203                    parsed.markup,
4204                ));
4205                self.boundary = self.boundary.max(6);
4206            }
4207            b"val" | b"yVal" => {
4208                if name == b"yVal" {
4209                    self.saw_scatter_wrapper = true;
4210                } else {
4211                    self.saw_standard_wrapper = true;
4212                }
4213                let wrapper = if name == b"yVal" {
4214                    b"yVal".as_slice()
4215                } else {
4216                    b"val".as_slice()
4217                };
4218                let parsed = parse_wrapper(&raw, wrapper, &[b"numRef"], namespaces)?;
4219                let (_, reference) = parsed.choice.ok_or_else(|| {
4220                    ChartError::MissingElement(format!(
4221                        "c:{}/c:numRef",
4222                        String::from_utf8_lossy(wrapper)
4223                    ))
4224                })?;
4225                set_once(
4226                    &mut self.values,
4227                    (
4228                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4229                        parsed.markup,
4230                    ),
4231                    "c:val or c:yVal",
4232                )?;
4233                self.boundary = self.boundary.max(7);
4234            }
4235            b"bubbleSize" => {
4236                mark_once(&mut self.bubble_size_seen, "c:bubbleSize")?;
4237                let parsed = parse_wrapper(&raw, b"bubbleSize", &[b"numRef"], namespaces)?;
4238                if let Some((_, reference)) = parsed.choice {
4239                    self.bubble_size = Some((
4240                        NumericData::from_xml_with_namespaces(&reference, &parsed.namespaces)?,
4241                        parsed.markup,
4242                    ));
4243                } else {
4244                    self.opaque_bubble_size = true;
4245                    self.raw_children.push(7, raw);
4246                }
4247                self.boundary = self.boundary.max(8);
4248            }
4249            _ => {
4250                let boundary = series_raw_boundary(name, self.boundary);
4251                self.raw_children.push(boundary, raw);
4252                self.boundary = self.boundary.max(boundary);
4253            }
4254        }
4255        Ok(())
4256    }
4257
4258    fn finish(
4259        self,
4260        raw_attributes: XmlAttributes,
4261        namespace_declarations: XmlAttributes,
4262    ) -> Result<Series> {
4263        if self.saw_standard_wrapper && self.saw_scatter_wrapper {
4264            return Err(ChartError::InvalidValue {
4265                element: "c:ser".to_owned(),
4266                value: "category/value and x/y wrappers cannot be combined".to_owned(),
4267            });
4268        }
4269        let (index, index_markup) = self
4270            .index
4271            .ok_or_else(|| ChartError::MissingElement("c:idx".to_owned()))?;
4272        let (order, order_markup) = self
4273            .order
4274            .ok_or_else(|| ChartError::MissingElement("c:order".to_owned()))?;
4275        let (values, values_markup) = self
4276            .values
4277            .ok_or_else(|| ChartError::MissingElement("c:val".to_owned()))?;
4278        let (name, name_markup) = self
4279            .name
4280            .map(|(value, markup)| (Some(value), Some(markup)))
4281            .unwrap_or((None, None));
4282        let (categories, categories_markup) = self
4283            .categories
4284            .map(|(value, markup)| (Some(value), Some(markup)))
4285            .unwrap_or((None, None));
4286        let (bubble_size, bubble_size_markup) = self
4287            .bubble_size
4288            .map(|(value, markup)| (Some(value), Some(markup)))
4289            .unwrap_or((None, None));
4290        Ok(Series {
4291            index,
4292            order,
4293            name,
4294            categories,
4295            values,
4296            bubble_size,
4297            sp_pr: self.sp_pr,
4298            data_labels: self.data_labels,
4299            index_markup,
4300            order_markup,
4301            name_markup,
4302            categories_markup,
4303            values_markup,
4304            bubble_size_markup,
4305            opaque_name: self.opaque_name,
4306            opaque_categories: self.opaque_categories,
4307            opaque_bubble_size: self.opaque_bubble_size,
4308            uses_scatter_wrappers: self.saw_scatter_wrapper,
4309            parsed_from_xml: true,
4310            raw_attributes,
4311            namespace_declarations,
4312            raw_children: raw_children_in_schema_order(&self.raw_children, 8),
4313        })
4314    }
4315}
4316
4317fn series_raw_boundary(name: &[u8], current: usize) -> usize {
4318    match name {
4319        b"marker" | b"invertIfNegative" | b"pictureOptions" | b"explosion" | b"dPt" => 4,
4320        b"trendline" | b"errBars" => 5,
4321        b"shape" | b"smooth" => 7,
4322        b"extLst" => 8,
4323        _ => current,
4324    }
4325}
4326
4327struct ParsedReference {
4328    formula: String,
4329    format_code: Option<String>,
4330    values: Vec<String>,
4331    markup: ReferenceMarkup,
4332}
4333
4334fn parse_reference(
4335    xml: &[u8],
4336    reference_local: &[u8],
4337    cache_local: &[u8],
4338    numeric: bool,
4339    inherited: &NamespaceBindings,
4340) -> Result<ParsedReference> {
4341    let mut reader = Reader::from_reader(xml);
4342    let mut buffer = Vec::new();
4343    loop {
4344        match reader
4345            .read_event_into(&mut buffer)
4346            .map_err(OxmlError::from)?
4347        {
4348            Event::Start(element)
4349                if matches_local_name(element.name().as_ref(), reference_local) =>
4350            {
4351                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4352                let raw_attributes = capture_fixed_attributes(&element, &["xmlns:c"])?;
4353                let mut formula: Option<(String, TextMarkup)> = None;
4354                let mut cache: Option<(Option<String>, Vec<String>, CacheMarkup)> = None;
4355                let mut raw_children = OrderedRawChildren::default();
4356                let mut boundary = 0usize;
4357                let mut inner = Vec::new();
4358                loop {
4359                    match reader
4360                        .read_event_into(&mut inner)
4361                        .map_err(OxmlError::from)?
4362                    {
4363                        Event::Start(child) => {
4364                            let name = chart_child_local(&child, &namespaces)?;
4365                            let raw = capture_element(&mut reader, &child)?;
4366                            match name.as_deref().unwrap_or_default() {
4367                                b"f" => {
4368                                    set_once(
4369                                        &mut formula,
4370                                        parse_text_element(&raw, b"f", &namespaces)?,
4371                                        "c:f",
4372                                    )?;
4373                                    boundary = boundary.max(1);
4374                                }
4375                                name if name == cache_local => {
4376                                    set_once(
4377                                        &mut cache,
4378                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4379                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4380                                    )?;
4381                                    boundary = boundary.max(2);
4382                                }
4383                                _ => raw_children.push(boundary, raw),
4384                            }
4385                        }
4386                        Event::Empty(child) => {
4387                            let name = chart_child_local(&child, &namespaces)?;
4388                            let raw = capture_empty_element(&child)?;
4389                            match name.as_deref().unwrap_or_default() {
4390                                b"f" => {
4391                                    set_once(
4392                                        &mut formula,
4393                                        parse_text_element(&raw, b"f", &namespaces)?,
4394                                        "c:f",
4395                                    )?;
4396                                    boundary = boundary.max(1);
4397                                }
4398                                name if name == cache_local => {
4399                                    set_once(
4400                                        &mut cache,
4401                                        parse_cache(&raw, cache_local, numeric, &namespaces)?,
4402                                        &format!("c:{}", String::from_utf8_lossy(cache_local)),
4403                                    )?;
4404                                    boundary = boundary.max(2);
4405                                }
4406                                _ => raw_children.push(boundary, raw),
4407                            }
4408                        }
4409                        event @ (Event::Text(_)
4410                        | Event::CData(_)
4411                        | Event::Comment(_)
4412                        | Event::PI(_)
4413                        | Event::GeneralRef(_)) => {
4414                            raw_children.push(boundary, capture_event(event)?);
4415                        }
4416                        Event::End(end)
4417                            if matches_local_name(end.name().as_ref(), reference_local) =>
4418                        {
4419                            let (formula, formula_markup) = formula
4420                                .ok_or_else(|| ChartError::MissingElement("c:f".to_owned()))?;
4421                            validate_formula(&formula, "c:f")?;
4422                            let (format_code, values, cache_markup) = cache.ok_or_else(|| {
4423                                ChartError::MissingElement(format!(
4424                                    "c:{}",
4425                                    String::from_utf8_lossy(cache_local)
4426                                ))
4427                            })?;
4428                            let markup = ReferenceMarkup {
4429                                raw_attributes,
4430                                raw_children: raw_children_in_schema_order(&raw_children, 2),
4431                                formula: formula_markup,
4432                                cache_attributes: cache_markup.raw_attributes,
4433                                cache_children: cache_markup.raw_children,
4434                                format_code: cache_markup.format_code,
4435                                point_count: cache_markup.point_count,
4436                                declared_point_count: cache_markup.declared_point_count,
4437                                point_indexes: cache_markup.point_indexes,
4438                                points: cache_markup.points,
4439                            };
4440                            return Ok(ParsedReference {
4441                                formula,
4442                                format_code,
4443                                values,
4444                                markup,
4445                            });
4446                        }
4447                        Event::Eof => {
4448                            return Err(missing_end(&format!(
4449                                "c:{}",
4450                                String::from_utf8_lossy(reference_local)
4451                            )));
4452                        }
4453                        _ => {}
4454                    }
4455                    inner.clear();
4456                }
4457            }
4458            Event::Empty(element)
4459                if matches_local_name(element.name().as_ref(), reference_local) =>
4460            {
4461                return Err(ChartError::MissingElement("c:f".to_owned()));
4462            }
4463            Event::Start(element) | Event::Empty(element) => {
4464                return Err(ChartError::UnexpectedElement(element_name(&element)));
4465            }
4466            Event::Eof => {
4467                return Err(ChartError::MissingElement(format!(
4468                    "c:{}",
4469                    String::from_utf8_lossy(reference_local)
4470                )));
4471            }
4472            _ => {}
4473        }
4474        buffer.clear();
4475    }
4476}
4477
4478struct CacheMarkup {
4479    raw_attributes: XmlAttributes,
4480    raw_children: OrderedRawChildren,
4481    format_code: Option<TextMarkup>,
4482    point_count: ScalarMarkup,
4483    declared_point_count: Option<u32>,
4484    point_indexes: Vec<u32>,
4485    points: Vec<PointMarkup>,
4486}
4487
4488fn parse_cache(
4489    xml: &[u8],
4490    cache_local: &[u8],
4491    numeric: bool,
4492    inherited: &NamespaceBindings,
4493) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4494    let mut reader = Reader::from_reader(xml);
4495    let mut buffer = Vec::new();
4496    loop {
4497        match reader
4498            .read_event_into(&mut buffer)
4499            .map_err(OxmlError::from)?
4500        {
4501            Event::Start(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4502                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4503                let raw_attributes = capture_attributes(&element)?;
4504                let mut state = CacheParseState::new(numeric, namespaces);
4505                let mut inner = Vec::new();
4506                loop {
4507                    match reader
4508                        .read_event_into(&mut inner)
4509                        .map_err(OxmlError::from)?
4510                    {
4511                        Event::Start(child) => {
4512                            let name = chart_child_local(&child, &state.namespaces)?;
4513                            let raw = capture_element(&mut reader, &child)?;
4514                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4515                        }
4516                        Event::Empty(child) => {
4517                            let name = chart_child_local(&child, &state.namespaces)?;
4518                            let raw = capture_empty_element(&child)?;
4519                            state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
4520                        }
4521                        event @ (Event::Text(_)
4522                        | Event::CData(_)
4523                        | Event::Comment(_)
4524                        | Event::PI(_)
4525                        | Event::GeneralRef(_)) => {
4526                            state.capture_event(capture_event(event)?);
4527                        }
4528                        Event::End(end) if matches_local_name(end.name().as_ref(), cache_local) => {
4529                            return state.finish(raw_attributes);
4530                        }
4531                        Event::Eof => {
4532                            return Err(missing_end(&format!(
4533                                "c:{}",
4534                                String::from_utf8_lossy(cache_local)
4535                            )));
4536                        }
4537                        _ => {}
4538                    }
4539                    inner.clear();
4540                }
4541            }
4542            Event::Empty(element) if matches_local_name(element.name().as_ref(), cache_local) => {
4543                return Err(ChartError::MissingElement("c:ptCount".to_owned()));
4544            }
4545            Event::Start(element) | Event::Empty(element) => {
4546                return Err(ChartError::UnexpectedElement(element_name(&element)));
4547            }
4548            Event::Eof => {
4549                return Err(ChartError::MissingElement(format!(
4550                    "c:{}",
4551                    String::from_utf8_lossy(cache_local)
4552                )));
4553            }
4554            _ => {}
4555        }
4556        buffer.clear();
4557    }
4558}
4559
4560struct CacheParseState {
4561    numeric: bool,
4562    namespaces: NamespaceBindings,
4563    point_base: usize,
4564    boundary: usize,
4565    format_code: Option<(String, TextMarkup)>,
4566    point_count: Option<(u32, ScalarMarkup)>,
4567    points: Vec<(u32, String, PointMarkup)>,
4568    raw_children: OrderedRawChildren,
4569}
4570
4571impl CacheParseState {
4572    fn new(numeric: bool, namespaces: NamespaceBindings) -> Self {
4573        Self {
4574            numeric,
4575            namespaces,
4576            point_base: usize::from(numeric) + 1,
4577            boundary: 0,
4578            format_code: None,
4579            point_count: None,
4580            points: Vec::new(),
4581            raw_children: OrderedRawChildren::default(),
4582        }
4583    }
4584
4585    fn capture_event(&mut self, raw: Vec<u8>) {
4586        self.raw_children.push(self.boundary, raw);
4587    }
4588
4589    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
4590        match name {
4591            b"formatCode" if self.numeric => {
4592                set_once(
4593                    &mut self.format_code,
4594                    parse_text_element(&raw, b"formatCode", &self.namespaces)?,
4595                    "c:formatCode",
4596                )?;
4597                self.boundary = self.boundary.max(1);
4598            }
4599            b"ptCount" => {
4600                set_once(
4601                    &mut self.point_count,
4602                    parse_u32_scalar(&raw, "ptCount")?,
4603                    "c:ptCount",
4604                )?;
4605                self.boundary = self.boundary.max(self.point_base);
4606            }
4607            b"pt" => {
4608                self.points.push(parse_point(&raw, &self.namespaces)?);
4609                self.boundary = self.boundary.max(self.point_base + self.points.len());
4610            }
4611            _ => self.raw_children.push(self.boundary, raw),
4612        }
4613        Ok(())
4614    }
4615
4616    fn finish(
4617        self,
4618        raw_attributes: XmlAttributes,
4619    ) -> Result<(Option<String>, Vec<String>, CacheMarkup)> {
4620        let (declared, point_count_markup) = self
4621            .point_count
4622            .ok_or_else(|| ChartError::MissingElement("c:ptCount".to_owned()))?;
4623        let actual = u32::try_from(self.points.len()).map_err(|_| ChartError::InvalidValue {
4624            element: "c:ptCount".to_owned(),
4625            value: self.points.len().to_string(),
4626        })?;
4627        if declared < actual {
4628            return Err(invalid_attribute("ptCount", "val", declared.to_string()));
4629        }
4630        let mut values = Vec::with_capacity(self.points.len());
4631        let mut point_indexes = Vec::with_capacity(self.points.len());
4632        let mut point_markup = Vec::with_capacity(self.points.len());
4633        let mut previous = None;
4634        for (index, value, markup) in self.points {
4635            if index >= declared || previous.is_some_and(|last| index <= last) {
4636                return Err(invalid_attribute("pt", "idx", index.to_string()));
4637            }
4638            previous = Some(index);
4639            values.push(value);
4640            point_indexes.push(index);
4641            point_markup.push(markup);
4642        }
4643        let (format_code, format_markup) = self
4644            .format_code
4645            .map(|(value, markup)| (Some(value), Some(markup)))
4646            .unwrap_or((None, None));
4647        if self.numeric && format_code.is_none() {
4648            return Err(ChartError::MissingElement("c:formatCode".to_owned()));
4649        }
4650        Ok((
4651            format_code,
4652            values,
4653            CacheMarkup {
4654                raw_attributes,
4655                raw_children: raw_children_in_schema_order(
4656                    &self.raw_children,
4657                    self.point_base + point_markup.len(),
4658                ),
4659                format_code: format_markup,
4660                point_count: point_count_markup,
4661                declared_point_count: Some(declared),
4662                point_indexes,
4663                points: point_markup,
4664            },
4665        ))
4666    }
4667}
4668
4669fn parse_point(xml: &[u8], inherited: &NamespaceBindings) -> Result<(u32, String, PointMarkup)> {
4670    let mut reader = Reader::from_reader(xml);
4671    let mut buffer = Vec::new();
4672    loop {
4673        match reader
4674            .read_event_into(&mut buffer)
4675            .map_err(OxmlError::from)?
4676        {
4677            Event::Start(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4678                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4679                let index = required_u32_attribute(&element, "pt", b"idx")?;
4680                let raw_attributes = capture_attributes_except(&element, b"idx")?;
4681                let mut value: Option<(String, TextMarkup)> = None;
4682                let mut raw_children = OrderedRawChildren::default();
4683                let mut boundary = 0usize;
4684                let mut inner = Vec::new();
4685                loop {
4686                    match reader
4687                        .read_event_into(&mut inner)
4688                        .map_err(OxmlError::from)?
4689                    {
4690                        Event::Start(child) => {
4691                            let name = chart_child_local(&child, &namespaces)?;
4692                            let raw = capture_element(&mut reader, &child)?;
4693                            if name.as_deref() == Some(b"v") {
4694                                set_once(
4695                                    &mut value,
4696                                    parse_text_element(&raw, b"v", &namespaces)?,
4697                                    "c:v",
4698                                )?;
4699                                boundary = 1;
4700                            } else {
4701                                raw_children.push(boundary, raw);
4702                            }
4703                        }
4704                        Event::Empty(child) => {
4705                            let name = chart_child_local(&child, &namespaces)?;
4706                            let raw = capture_empty_element(&child)?;
4707                            if name.as_deref() == Some(b"v") {
4708                                set_once(
4709                                    &mut value,
4710                                    parse_text_element(&raw, b"v", &namespaces)?,
4711                                    "c:v",
4712                                )?;
4713                                boundary = 1;
4714                            } else {
4715                                raw_children.push(boundary, raw);
4716                            }
4717                        }
4718                        event @ (Event::Text(_)
4719                        | Event::CData(_)
4720                        | Event::Comment(_)
4721                        | Event::PI(_)
4722                        | Event::GeneralRef(_)) => {
4723                            raw_children.push(boundary, capture_event(event)?);
4724                        }
4725                        Event::End(end) if matches_local_name(end.name().as_ref(), b"pt") => {
4726                            let (value, value_markup) = value
4727                                .ok_or_else(|| ChartError::MissingElement("c:v".to_owned()))?;
4728                            return Ok((
4729                                index,
4730                                value,
4731                                PointMarkup {
4732                                    raw_attributes,
4733                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
4734                                    value: value_markup,
4735                                },
4736                            ));
4737                        }
4738                        Event::Eof => return Err(missing_end("c:pt")),
4739                        _ => {}
4740                    }
4741                    inner.clear();
4742                }
4743            }
4744            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pt") => {
4745                return Err(ChartError::MissingElement("c:v".to_owned()));
4746            }
4747            Event::Start(element) | Event::Empty(element) => {
4748                return Err(ChartError::UnexpectedElement(element_name(&element)));
4749            }
4750            Event::Eof => return Err(ChartError::MissingElement("c:pt".to_owned())),
4751            _ => {}
4752        }
4753        buffer.clear();
4754    }
4755}
4756
4757fn parse_text_element(
4758    xml: &[u8],
4759    local: &[u8],
4760    inherited: &NamespaceBindings,
4761) -> Result<(String, TextMarkup)> {
4762    let mut reader = Reader::from_reader(xml);
4763    let mut buffer = Vec::new();
4764    loop {
4765        match reader
4766            .read_event_into(&mut buffer)
4767            .map_err(OxmlError::from)?
4768        {
4769            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
4770                typed_rewrite_bindings(&element, inherited)?;
4771                let raw_attributes = capture_attributes(&element)?;
4772                let mut value = String::new();
4773                let mut raw_children = OrderedRawChildren::default();
4774                let mut seen_text = false;
4775                let mut inner = Vec::new();
4776                loop {
4777                    match reader
4778                        .read_event_into(&mut inner)
4779                        .map_err(OxmlError::from)?
4780                    {
4781                        Event::Text(text) => {
4782                            value.push_str(&decode_plain(&text));
4783                            seen_text = true;
4784                        }
4785                        Event::CData(text) => {
4786                            let decoded =
4787                                text.decode().map_err(|error| ChartError::InvalidValue {
4788                                    element: format!("c:{}", String::from_utf8_lossy(local)),
4789                                    value: error.to_string(),
4790                                })?;
4791                            value.push_str(&decoded);
4792                            seen_text = true;
4793                        }
4794                        Event::GeneralRef(reference) => {
4795                            value.push_str(&resolve_entity(&reference));
4796                            seen_text = true;
4797                        }
4798                        event @ (Event::Comment(_) | Event::PI(_)) => {
4799                            raw_children.push(usize::from(seen_text), capture_event(event)?);
4800                        }
4801                        Event::Start(child) | Event::Empty(child) => {
4802                            return Err(ChartError::UnexpectedElement(element_name(&child)));
4803                        }
4804                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
4805                            return Ok((
4806                                value,
4807                                TextMarkup {
4808                                    raw_attributes,
4809                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
4810                                },
4811                            ));
4812                        }
4813                        Event::Eof => {
4814                            return Err(missing_end(&format!(
4815                                "c:{}",
4816                                String::from_utf8_lossy(local)
4817                            )));
4818                        }
4819                        _ => {}
4820                    }
4821                    inner.clear();
4822                }
4823            }
4824            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
4825                typed_rewrite_bindings(&element, inherited)?;
4826                return Ok((
4827                    String::new(),
4828                    TextMarkup {
4829                        raw_attributes: capture_attributes(&element)?,
4830                        raw_children: OrderedRawChildren::default(),
4831                    },
4832                ));
4833            }
4834            Event::Start(element) | Event::Empty(element) => {
4835                return Err(ChartError::UnexpectedElement(element_name(&element)));
4836            }
4837            Event::Eof => {
4838                return Err(ChartError::MissingElement(format!(
4839                    "c:{}",
4840                    String::from_utf8_lossy(local)
4841                )));
4842            }
4843            _ => {}
4844        }
4845        buffer.clear();
4846    }
4847}
4848
4849struct ParsedWrapper {
4850    choice: Option<(Vec<u8>, Vec<u8>)>,
4851    markup: WrapperMarkup,
4852    namespaces: NamespaceBindings,
4853}
4854
4855fn parse_wrapper(
4856    xml: &[u8],
4857    local: &[u8],
4858    choices: &[&[u8]],
4859    inherited: &NamespaceBindings,
4860) -> Result<ParsedWrapper> {
4861    let mut reader = Reader::from_reader(xml);
4862    let mut buffer = Vec::new();
4863    loop {
4864        match reader
4865            .read_event_into(&mut buffer)
4866            .map_err(OxmlError::from)?
4867        {
4868            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
4869                let namespaces = typed_rewrite_bindings(&element, inherited)?;
4870                let raw_attributes = capture_attributes(&element)?;
4871                let mut choice = None;
4872                let mut raw_children = OrderedRawChildren::default();
4873                let mut boundary = 0usize;
4874                let mut inner = Vec::new();
4875                loop {
4876                    match reader
4877                        .read_event_into(&mut inner)
4878                        .map_err(OxmlError::from)?
4879                    {
4880                        Event::Start(child) => {
4881                            let name = chart_child_local(&child, &namespaces)?;
4882                            let raw = capture_element(&mut reader, &child)?;
4883                            if let Some(name) =
4884                                name.filter(|name| choices.contains(&name.as_slice()))
4885                            {
4886                                if choice.is_some() {
4887                                    return Err(ChartError::DuplicateElement(format!(
4888                                        "c:{} reference",
4889                                        String::from_utf8_lossy(local)
4890                                    )));
4891                                }
4892                                choice = Some((name, raw));
4893                                boundary = 1;
4894                            } else {
4895                                raw_children.push(boundary, raw);
4896                            }
4897                        }
4898                        Event::Empty(child) => {
4899                            let name = chart_child_local(&child, &namespaces)?;
4900                            let raw = capture_empty_element(&child)?;
4901                            if let Some(name) =
4902                                name.filter(|name| choices.contains(&name.as_slice()))
4903                            {
4904                                if choice.is_some() {
4905                                    return Err(ChartError::DuplicateElement(format!(
4906                                        "c:{} reference",
4907                                        String::from_utf8_lossy(local)
4908                                    )));
4909                                }
4910                                choice = Some((name, raw));
4911                                boundary = 1;
4912                            } else {
4913                                raw_children.push(boundary, raw);
4914                            }
4915                        }
4916                        event @ (Event::Text(_)
4917                        | Event::CData(_)
4918                        | Event::Comment(_)
4919                        | Event::PI(_)
4920                        | Event::GeneralRef(_)) => {
4921                            raw_children.push(boundary, capture_event(event)?);
4922                        }
4923                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
4924                            return Ok(ParsedWrapper {
4925                                choice,
4926                                markup: WrapperMarkup {
4927                                    raw_attributes,
4928                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
4929                                },
4930                                namespaces,
4931                            });
4932                        }
4933                        Event::Eof => {
4934                            return Err(missing_end(&format!(
4935                                "c:{}",
4936                                String::from_utf8_lossy(local)
4937                            )));
4938                        }
4939                        _ => {}
4940                    }
4941                    inner.clear();
4942                }
4943            }
4944            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
4945                let namespaces = chart_bindings(inherited, &element)?;
4946                return Ok(ParsedWrapper {
4947                    choice: None,
4948                    markup: WrapperMarkup {
4949                        raw_attributes: capture_attributes(&element)?,
4950                        raw_children: OrderedRawChildren::default(),
4951                    },
4952                    namespaces,
4953                });
4954            }
4955            Event::Start(element) | Event::Empty(element) => {
4956                return Err(ChartError::UnexpectedElement(element_name(&element)));
4957            }
4958            Event::Eof => {
4959                return Err(ChartError::MissingElement(format!(
4960                    "c:{}",
4961                    String::from_utf8_lossy(local)
4962                )));
4963            }
4964            _ => {}
4965        }
4966        buffer.clear();
4967    }
4968}
4969
4970fn write_string_cache(
4971    writer: &mut Writer<Vec<u8>>,
4972    values: &[String],
4973    markup: &ReferenceMarkup,
4974) -> Result<()> {
4975    let (declared_count, indexes) = cache_layout(markup, values.len())?;
4976    let mut start = BytesStart::new("c:strCache");
4977    push_attributes(&mut start, &markup.cache_attributes);
4978    writer
4979        .write_event(Event::Start(start))
4980        .map_err(OxmlError::from)?;
4981    emit_raw(writer, markup.cache_children.at(0))?;
4982    write_scalar(
4983        writer,
4984        "c:ptCount",
4985        &declared_count.to_string(),
4986        Some(&markup.point_count),
4987    )?;
4988    emit_cache_point_raw(writer, markup, 1, values.len(), 0)?;
4989    for (position, value) in values.iter().enumerate() {
4990        let default_markup = PointMarkup::default();
4991        write_point(
4992            writer,
4993            indexes[position],
4994            value,
4995            markup.points.get(position).unwrap_or(&default_markup),
4996        )?;
4997        emit_cache_point_raw(writer, markup, 1, values.len(), position + 1)?;
4998    }
4999    writer
5000        .write_event(Event::End(BytesEnd::new("c:strCache")))
5001        .map_err(OxmlError::from)?;
5002    Ok(())
5003}
5004
5005fn write_numeric_cache(
5006    writer: &mut Writer<Vec<u8>>,
5007    data: &NumericData,
5008    markup: &ReferenceMarkup,
5009) -> Result<()> {
5010    let (declared_count, indexes) = cache_layout(markup, data.values.len())?;
5011    let mut start = BytesStart::new("c:numCache");
5012    push_attributes(&mut start, &markup.cache_attributes);
5013    writer
5014        .write_event(Event::Start(start))
5015        .map_err(OxmlError::from)?;
5016    emit_raw(writer, markup.cache_children.at(0))?;
5017    let default_format_markup = TextMarkup::default();
5018    write_text(
5019        writer,
5020        "c:formatCode",
5021        &data.format_code,
5022        markup
5023            .format_code
5024            .as_ref()
5025            .unwrap_or(&default_format_markup),
5026    )?;
5027    emit_raw(writer, markup.cache_children.at(1))?;
5028    write_scalar(
5029        writer,
5030        "c:ptCount",
5031        &declared_count.to_string(),
5032        Some(&markup.point_count),
5033    )?;
5034    emit_cache_point_raw(writer, markup, 2, data.values.len(), 0)?;
5035    for (position, value) in data.values.iter().enumerate() {
5036        let default_markup = PointMarkup::default();
5037        write_point(
5038            writer,
5039            indexes[position],
5040            &value.to_string(),
5041            markup.points.get(position).unwrap_or(&default_markup),
5042        )?;
5043        emit_cache_point_raw(writer, markup, 2, data.values.len(), position + 1)?;
5044    }
5045    writer
5046        .write_event(Event::End(BytesEnd::new("c:numCache")))
5047        .map_err(OxmlError::from)?;
5048    Ok(())
5049}
5050
5051fn emit_cache_point_raw(
5052    writer: &mut Writer<Vec<u8>>,
5053    markup: &ReferenceMarkup,
5054    point_base: usize,
5055    current_count: usize,
5056    completed_points: usize,
5057) -> Result<()> {
5058    let original_count = markup.points.len();
5059    if current_count == 0 {
5060        for boundary in point_base..=point_base + original_count {
5061            emit_raw(writer, markup.cache_children.at(boundary))?;
5062        }
5063    } else if completed_points == 0 {
5064        if original_count > 0 {
5065            emit_raw(writer, markup.cache_children.at(point_base))?;
5066        }
5067    } else if completed_points < current_count {
5068        if completed_points < original_count {
5069            emit_raw(
5070                writer,
5071                markup.cache_children.at(point_base + completed_points),
5072            )?;
5073        }
5074    } else {
5075        let first_tail = if original_count == 0 {
5076            point_base
5077        } else {
5078            point_base + current_count.min(original_count)
5079        };
5080        for boundary in first_tail..=point_base + original_count {
5081            emit_raw(writer, markup.cache_children.at(boundary))?;
5082        }
5083    }
5084    Ok(())
5085}
5086
5087fn write_point(
5088    writer: &mut Writer<Vec<u8>>,
5089    index: u32,
5090    value: &str,
5091    markup: &PointMarkup,
5092) -> Result<()> {
5093    let index = index.to_string();
5094    let mut start = BytesStart::new("c:pt");
5095    start.push_attribute(("idx", index.as_str()));
5096    push_attributes(&mut start, &markup.raw_attributes);
5097    writer
5098        .write_event(Event::Start(start))
5099        .map_err(OxmlError::from)?;
5100    emit_raw(writer, markup.raw_children.at(0))?;
5101    write_text(writer, "c:v", value, &markup.value)?;
5102    emit_raw(writer, markup.raw_children.at(1))?;
5103    writer
5104        .write_event(Event::End(BytesEnd::new("c:pt")))
5105        .map_err(OxmlError::from)?;
5106    Ok(())
5107}
5108
5109fn cache_layout(markup: &ReferenceMarkup, value_count: usize) -> Result<(u32, Vec<u32>)> {
5110    if markup.point_indexes.len() == value_count
5111        && let Some(declared) = markup.declared_point_count
5112    {
5113        return Ok((declared, markup.point_indexes.clone()));
5114    }
5115    let declared = u32::try_from(value_count).map_err(|_| ChartError::InvalidValue {
5116        element: "c:ptCount".to_owned(),
5117        value: value_count.to_string(),
5118    })?;
5119    Ok((declared, (0..declared).collect()))
5120}
5121
5122fn write_text(
5123    writer: &mut Writer<Vec<u8>>,
5124    tag: &str,
5125    value: &str,
5126    markup: &TextMarkup,
5127) -> Result<()> {
5128    let mut start = BytesStart::new(tag);
5129    push_attributes(&mut start, &markup.raw_attributes);
5130    writer
5131        .write_event(Event::Start(start))
5132        .map_err(OxmlError::from)?;
5133    emit_raw(writer, markup.raw_children.at(0))?;
5134    writer
5135        .write_event(Event::Text(BytesText::new(value)))
5136        .map_err(OxmlError::from)?;
5137    emit_raw(writer, markup.raw_children.at(1))?;
5138    writer
5139        .write_event(Event::End(BytesEnd::new(tag)))
5140        .map_err(OxmlError::from)?;
5141    Ok(())
5142}
5143
5144fn write_wrapper_start(
5145    writer: &mut Writer<Vec<u8>>,
5146    tag: &str,
5147    markup: &WrapperMarkup,
5148) -> Result<()> {
5149    let mut start = BytesStart::new(tag);
5150    push_attributes(&mut start, &markup.raw_attributes);
5151    writer
5152        .write_event(Event::Start(start))
5153        .map_err(OxmlError::from)?;
5154    emit_raw(writer, markup.raw_children.at(0))
5155}
5156
5157fn write_wrapper_end(
5158    writer: &mut Writer<Vec<u8>>,
5159    tag: &str,
5160    markup: &WrapperMarkup,
5161) -> Result<()> {
5162    emit_raw(writer, markup.raw_children.at(1))?;
5163    writer
5164        .write_event(Event::End(BytesEnd::new(tag)))
5165        .map_err(OxmlError::from)?;
5166    Ok(())
5167}
5168
5169fn validate_formula(formula: &str, element: &str) -> Result<()> {
5170    if formula.trim().is_empty() {
5171        return Err(ChartError::InvalidValue {
5172            element: element.to_owned(),
5173            value: formula.to_owned(),
5174        });
5175    }
5176    validate_xml_text(formula, element)
5177}
5178
5179fn validate_format_code(format_code: &str) -> Result<()> {
5180    if format_code.is_empty() {
5181        return Err(ChartError::InvalidValue {
5182            element: "c:formatCode".to_owned(),
5183            value: format_code.to_owned(),
5184        });
5185    }
5186    validate_xml_text(format_code, "c:formatCode")
5187}
5188
5189fn validate_xml_text(value: &str, element: &str) -> Result<()> {
5190    if value.chars().all(|character| {
5191        matches!(
5192            character,
5193            '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
5194        )
5195    }) {
5196        return Ok(());
5197    }
5198    Err(ChartError::InvalidValue {
5199        element: element.to_owned(),
5200        value: value.to_owned(),
5201    })
5202}
5203
5204fn validate_numeric_values(values: &[f64]) -> Result<()> {
5205    if let Some(value) = values.iter().find(|value| !value.is_finite()) {
5206        return Err(ChartError::InvalidValue {
5207            element: "c:v".to_owned(),
5208            value: value.to_string(),
5209        });
5210    }
5211    Ok(())
5212}
5213
5214fn validate_point_count(count: usize, element: &str) -> Result<()> {
5215    u32::try_from(count)
5216        .map(|_| ())
5217        .map_err(|_| ChartError::InvalidValue {
5218            element: element.to_owned(),
5219            value: count.to_string(),
5220        })
5221}
5222
5223fn parse_u32_scalar(xml: &[u8], local: &str) -> Result<(u32, ScalarMarkup)> {
5224    let (value, markup) = scalar_value(xml, local)?;
5225    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
5226    let parsed = value
5227        .parse::<u32>()
5228        .map_err(|_| invalid_attribute(local, "val", value))?;
5229    Ok((parsed, markup))
5230}
5231
5232fn required_u32_attribute(element: &BytesStart<'_>, local: &str, attribute: &[u8]) -> Result<u32> {
5233    let value = attribute_value(element, attribute)?.ok_or_else(|| {
5234        invalid_attribute(
5235            local,
5236            &String::from_utf8_lossy(attribute),
5237            "<missing>".to_owned(),
5238        )
5239    })?;
5240    value
5241        .parse::<u32>()
5242        .map_err(|_| invalid_attribute(local, &String::from_utf8_lossy(attribute), value))
5243}
5244
5245fn chart_root_prefix(element: &BytesStart<'_>) -> Result<Vec<u8>> {
5246    let name = element.name();
5247    let qualified = name.as_ref();
5248    let prefix = qualified
5249        .iter()
5250        .position(|byte| *byte == b':')
5251        .map(|position| qualified[..position].to_vec())
5252        .unwrap_or_default();
5253    let declaration = if prefix.is_empty() {
5254        b"xmlns".to_vec()
5255    } else {
5256        let mut declaration = b"xmlns:".to_vec();
5257        declaration.extend_from_slice(&prefix);
5258        declaration
5259    };
5260    if let Some(namespace) = attribute_value(element, &declaration)?
5261        && namespace != C_NS
5262    {
5263        return Err(invalid_attribute(
5264            &element_name(element),
5265            "namespace",
5266            namespace,
5267        ));
5268    }
5269    reject_conflicting_prefix(element, b"c", C_NS)?;
5270    Ok(prefix)
5271}
5272
5273fn typed_rewrite_bindings(
5274    element: &BytesStart<'_>,
5275    inherited: &NamespaceBindings,
5276) -> Result<NamespaceBindings> {
5277    chart_root_prefix(element)?;
5278    if !element_is_in_namespace(element, C_NS, inherited)? {
5279        return Err(ChartError::UnexpectedElement(element_name(element)));
5280    }
5281    let bindings = chart_bindings(inherited, element)?;
5282    require_fixed_namespace(&bindings, b"c", C_NS, element)?;
5283    Ok(bindings)
5284}
5285
5286fn chart_namespace_defaults() -> NamespaceBindings {
5287    vec![
5288        (b"c".to_vec(), C_NS.to_owned()),
5289        (b"a".to_vec(), A_NS.to_owned()),
5290        (b"r".to_vec(), R_NS.to_owned()),
5291    ]
5292}
5293
5294fn chart_bindings(
5295    inherited: &NamespaceBindings,
5296    element: &BytesStart<'_>,
5297) -> Result<NamespaceBindings> {
5298    let mut bindings = chart_namespace_defaults();
5299    for (prefix, namespace) in inherited
5300        .iter()
5301        .cloned()
5302        .chain(namespace_bindings(element)?)
5303    {
5304        upsert_namespace_binding(&mut bindings, prefix, namespace);
5305    }
5306    Ok(bindings)
5307}
5308
5309fn root_chart_bindings(
5310    xml: &[u8],
5311    local: &[u8],
5312    inherited: &NamespaceBindings,
5313) -> Result<NamespaceBindings> {
5314    let mut reader = Reader::from_reader(xml);
5315    let mut buffer = Vec::new();
5316    loop {
5317        match reader
5318            .read_event_into(&mut buffer)
5319            .map_err(OxmlError::from)?
5320        {
5321            Event::Start(element) | Event::Empty(element)
5322                if matches_local_name(element.name().as_ref(), local) =>
5323            {
5324                if !element_is_in_namespace(&element, C_NS, inherited)? {
5325                    return Err(ChartError::UnexpectedElement(element_name(&element)));
5326                }
5327                return chart_bindings(inherited, &element);
5328            }
5329            Event::Start(element) | Event::Empty(element) => {
5330                return Err(ChartError::UnexpectedElement(element_name(&element)));
5331            }
5332            Event::Eof => {
5333                return Err(ChartError::MissingElement(format!(
5334                    "c:{}",
5335                    String::from_utf8_lossy(local)
5336                )));
5337            }
5338            _ => {}
5339        }
5340        buffer.clear();
5341    }
5342}
5343
5344fn chart_child_local(
5345    element: &BytesStart<'_>,
5346    inherited: &NamespaceBindings,
5347) -> Result<Option<Vec<u8>>> {
5348    Ok(element_is_in_namespace(element, C_NS, inherited)?
5349        .then(|| local_name(element.name().as_ref()).to_vec()))
5350}
5351
5352fn capture_fixed_root_attributes(
5353    start: &BytesStart<'_>,
5354    fixed: &[&str],
5355) -> Result<(XmlAttributes, XmlAttributes)> {
5356    let mut attributes = Vec::new();
5357    let mut namespaces = Vec::new();
5358    for attribute in start.attributes() {
5359        let attribute = attribute.map_err(OxmlError::from)?;
5360        let name = std::str::from_utf8(attribute.key.as_ref())
5361            .map_err(OxmlError::from)?
5362            .to_owned();
5363        let value = attribute
5364            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5365            .map_err(OxmlError::from)?
5366            .into_owned();
5367        if fixed.contains(&name.as_str()) {
5368            continue;
5369        }
5370        if name == "xmlns" || name.starts_with("xmlns:") {
5371            namespaces.push((name, value));
5372        } else {
5373            attributes.push((name, value));
5374        }
5375    }
5376    Ok((attributes, namespaces))
5377}
5378
5379fn require_fixed_namespace(
5380    bindings: &NamespaceBindings,
5381    prefix: &[u8],
5382    expected: &str,
5383    element: &BytesStart<'_>,
5384) -> Result<()> {
5385    let actual = bindings
5386        .iter()
5387        .find(|(candidate, _)| candidate.as_slice() == prefix)
5388        .map(|(_, namespace)| namespace.as_str());
5389    if actual == Some(expected) {
5390        return Ok(());
5391    }
5392    Err(invalid_attribute(
5393        &element_name(element),
5394        &format!("xmlns:{}", String::from_utf8_lossy(prefix)),
5395        actual.unwrap_or("<missing>").to_owned(),
5396    ))
5397}
5398
5399fn standalone_namespace_declarations(bindings: &NamespaceBindings) -> Result<XmlAttributes> {
5400    bindings
5401        .iter()
5402        .filter(|(prefix, _)| prefix != b"c" && prefix != b"a" && prefix != b"r")
5403        .map(|(prefix, namespace)| {
5404            let prefix = std::str::from_utf8(prefix).map_err(OxmlError::from)?;
5405            let name = if prefix.is_empty() {
5406                "xmlns".to_owned()
5407            } else {
5408                format!("xmlns:{prefix}")
5409            };
5410            Ok((name, namespace.clone()))
5411        })
5412        .collect()
5413}
5414
5415fn capture_fixed_attributes(start: &BytesStart<'_>, fixed: &[&str]) -> Result<XmlAttributes> {
5416    let mut attributes = Vec::new();
5417    for attribute in start.attributes() {
5418        let attribute = attribute.map_err(OxmlError::from)?;
5419        let name = std::str::from_utf8(attribute.key.as_ref())
5420            .map_err(OxmlError::from)?
5421            .to_owned();
5422        if fixed.contains(&name.as_str()) {
5423            continue;
5424        }
5425        let value = attribute
5426            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
5427            .map_err(OxmlError::from)?
5428            .into_owned();
5429        attributes.push((name, value));
5430    }
5431    Ok(attributes)
5432}
5433
5434/// A producer-compatible ChartML axis identifier.
5435///
5436/// ECMA-376 names an unsigned value, but PowerPoint emits signed 32-bit
5437/// lexical values in real chart parts. The accepted domain covers both forms.
5438#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5439pub struct AxisId(i64);
5440
5441impl AxisId {
5442    pub fn new(value: i64) -> Result<Self> {
5443        if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) {
5444            Ok(Self(value))
5445        } else {
5446            Err(ChartError::InvalidValue {
5447                element: "c:axId".to_owned(),
5448                value: value.to_string(),
5449            })
5450        }
5451    }
5452
5453    pub const fn value(self) -> i64 {
5454        self.0
5455    }
5456}
5457
5458/// Whether bars extend along the category or value axis.
5459#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5460pub enum BarDirection {
5461    Bar,
5462    Column,
5463}
5464
5465impl BarDirection {
5466    fn parse(value: &str) -> Option<Self> {
5467        match value {
5468            "bar" => Some(Self::Bar),
5469            "col" => Some(Self::Column),
5470            _ => None,
5471        }
5472    }
5473
5474    const fn as_str(self) -> &'static str {
5475        match self {
5476            Self::Bar => "bar",
5477            Self::Column => "col",
5478        }
5479    }
5480}
5481
5482/// Grouping modes supported by a two-dimensional bar plot.
5483#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5484pub enum BarGrouping {
5485    Clustered,
5486    PercentStacked,
5487    Stacked,
5488    Standard,
5489}
5490
5491impl BarGrouping {
5492    fn parse(value: &str) -> Option<Self> {
5493        match value {
5494            "clustered" => Some(Self::Clustered),
5495            "percentStacked" => Some(Self::PercentStacked),
5496            "stacked" => Some(Self::Stacked),
5497            "standard" => Some(Self::Standard),
5498            _ => None,
5499        }
5500    }
5501
5502    const fn as_str(self) -> &'static str {
5503        match self {
5504            Self::Clustered => "clustered",
5505            Self::PercentStacked => "percentStacked",
5506            Self::Stacked => "stacked",
5507            Self::Standard => "standard",
5508        }
5509    }
5510}
5511
5512/// Grouping modes shared by line and later area plots.
5513#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5514pub enum Grouping {
5515    PercentStacked,
5516    Stacked,
5517    Standard,
5518}
5519
5520impl Grouping {
5521    fn parse(value: &str) -> Option<Self> {
5522        match value {
5523            "percentStacked" => Some(Self::PercentStacked),
5524            "stacked" => Some(Self::Stacked),
5525            "standard" => Some(Self::Standard),
5526            _ => None,
5527        }
5528    }
5529
5530    const fn as_str(self) -> &'static str {
5531        match self {
5532            Self::PercentStacked => "percentStacked",
5533            Self::Stacked => "stacked",
5534            Self::Standard => "standard",
5535        }
5536    }
5537}
5538
5539/// Display style for a two-dimensional scatter plot.
5540#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5541pub enum ScatterStyle {
5542    Line,
5543    LineMarker,
5544    Marker,
5545    None,
5546    Smooth,
5547    SmoothMarker,
5548}
5549
5550impl ScatterStyle {
5551    fn parse(value: &str) -> Option<Self> {
5552        match value {
5553            "line" => Some(Self::Line),
5554            "lineMarker" => Some(Self::LineMarker),
5555            "marker" => Some(Self::Marker),
5556            "none" => Some(Self::None),
5557            "smooth" => Some(Self::Smooth),
5558            "smoothMarker" => Some(Self::SmoothMarker),
5559            _ => None,
5560        }
5561    }
5562
5563    const fn as_str(self) -> &'static str {
5564        match self {
5565            Self::Line => "line",
5566            Self::LineMarker => "lineMarker",
5567            Self::Marker => "marker",
5568            Self::None => "none",
5569            Self::Smooth => "smooth",
5570            Self::SmoothMarker => "smoothMarker",
5571        }
5572    }
5573}
5574
5575/// Display style for a two-dimensional radar plot.
5576#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5577pub enum RadarStyle {
5578    Filled,
5579    Marker,
5580    Standard,
5581}
5582
5583impl RadarStyle {
5584    fn parse(value: &str) -> Option<Self> {
5585        match value {
5586            "filled" => Some(Self::Filled),
5587            "marker" => Some(Self::Marker),
5588            "standard" => Some(Self::Standard),
5589            _ => None,
5590        }
5591    }
5592
5593    const fn as_str(self) -> &'static str {
5594        match self {
5595            Self::Filled => "filled",
5596            Self::Marker => "marker",
5597            Self::Standard => "standard",
5598        }
5599    }
5600}
5601
5602/// A supported two-dimensional plot owned by one plot area.
5603#[derive(Clone, Debug, PartialEq)]
5604pub enum Plot {
5605    Bar {
5606        direction: BarDirection,
5607        grouping: BarGrouping,
5608        gap_width: u16,
5609        overlap: i8,
5610        series: Vec<Series>,
5611        data_labels: Option<CT_DLbls>,
5612        axis_ids: [AxisId; 2],
5613    },
5614    Line {
5615        grouping: Grouping,
5616        marker: bool,
5617        smooth: bool,
5618        series: Vec<Series>,
5619        data_labels: Option<CT_DLbls>,
5620        axis_ids: [AxisId; 2],
5621    },
5622    Pie {
5623        first_slice_angle: u16,
5624        series: Vec<Series>,
5625        data_labels: Option<CT_DLbls>,
5626    },
5627    Doughnut {
5628        first_slice_angle: u16,
5629        hole_size: u8,
5630        series: Vec<Series>,
5631        data_labels: Option<CT_DLbls>,
5632    },
5633    Area {
5634        grouping: Grouping,
5635        series: Vec<Series>,
5636        data_labels: Option<CT_DLbls>,
5637        axis_ids: [AxisId; 2],
5638    },
5639    Scatter {
5640        style: ScatterStyle,
5641        series: Vec<Series>,
5642        data_labels: Option<CT_DLbls>,
5643        axis_ids: [AxisId; 2],
5644    },
5645    Radar {
5646        style: RadarStyle,
5647        series: Vec<Series>,
5648        data_labels: Option<CT_DLbls>,
5649        axis_ids: [AxisId; 2],
5650    },
5651}
5652
5653impl Plot {
5654    pub fn bar(
5655        direction: BarDirection,
5656        grouping: BarGrouping,
5657        series: Vec<Series>,
5658        axis_ids: [AxisId; 2],
5659    ) -> Result<Self> {
5660        let plot = Self::Bar {
5661            direction,
5662            grouping,
5663            gap_width: 150,
5664            overlap: 0,
5665            series,
5666            data_labels: None,
5667            axis_ids,
5668        };
5669        plot.validate()?;
5670        Ok(plot)
5671    }
5672
5673    pub fn line(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5674        let plot = Self::Line {
5675            grouping,
5676            marker: false,
5677            smooth: false,
5678            series,
5679            data_labels: None,
5680            axis_ids,
5681        };
5682        plot.validate()?;
5683        Ok(plot)
5684    }
5685
5686    pub fn pie(series: Vec<Series>) -> Result<Self> {
5687        let plot = Self::Pie {
5688            first_slice_angle: 0,
5689            series,
5690            data_labels: None,
5691        };
5692        plot.validate()?;
5693        Ok(plot)
5694    }
5695
5696    pub fn doughnut(series: Vec<Series>) -> Result<Self> {
5697        let plot = Self::Doughnut {
5698            first_slice_angle: 0,
5699            hole_size: 50,
5700            series,
5701            data_labels: None,
5702        };
5703        plot.validate()?;
5704        Ok(plot)
5705    }
5706
5707    pub fn area(grouping: Grouping, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5708        let plot = Self::Area {
5709            grouping,
5710            series,
5711            data_labels: None,
5712            axis_ids,
5713        };
5714        plot.validate()?;
5715        Ok(plot)
5716    }
5717
5718    pub fn scatter(
5719        style: ScatterStyle,
5720        series: Vec<Series>,
5721        axis_ids: [AxisId; 2],
5722    ) -> Result<Self> {
5723        let plot = Self::Scatter {
5724            style,
5725            series,
5726            data_labels: None,
5727            axis_ids,
5728        };
5729        plot.validate()?;
5730        Ok(plot)
5731    }
5732
5733    pub fn radar(style: RadarStyle, series: Vec<Series>, axis_ids: [AxisId; 2]) -> Result<Self> {
5734        let plot = Self::Radar {
5735            style,
5736            series,
5737            data_labels: None,
5738            axis_ids,
5739        };
5740        plot.validate()?;
5741        Ok(plot)
5742    }
5743
5744    fn axis_ids(&self) -> Option<[AxisId; 2]> {
5745        match self {
5746            Self::Bar { axis_ids, .. }
5747            | Self::Line { axis_ids, .. }
5748            | Self::Area { axis_ids, .. }
5749            | Self::Scatter { axis_ids, .. }
5750            | Self::Radar { axis_ids, .. } => Some(*axis_ids),
5751            Self::Pie { .. } | Self::Doughnut { .. } => None,
5752        }
5753    }
5754
5755    fn validate(&self) -> Result<()> {
5756        let (series, labels, axis_ids) = match self {
5757            Self::Bar {
5758                gap_width,
5759                overlap,
5760                series,
5761                data_labels,
5762                axis_ids,
5763                ..
5764            } => {
5765                if *gap_width > 500 {
5766                    return Err(ChartError::InvalidValue {
5767                        element: "c:gapWidth".to_owned(),
5768                        value: gap_width.to_string(),
5769                    });
5770                }
5771                if !(-100..=100).contains(overlap) {
5772                    return Err(ChartError::InvalidValue {
5773                        element: "c:overlap".to_owned(),
5774                        value: overlap.to_string(),
5775                    });
5776                }
5777                (series, data_labels, Some(axis_ids))
5778            }
5779            Self::Line {
5780                series,
5781                data_labels,
5782                axis_ids,
5783                ..
5784            } => (series, data_labels, Some(axis_ids)),
5785            Self::Pie {
5786                first_slice_angle,
5787                series,
5788                data_labels,
5789            } => {
5790                if *first_slice_angle > 360 {
5791                    return Err(ChartError::InvalidValue {
5792                        element: "c:firstSliceAng".to_owned(),
5793                        value: first_slice_angle.to_string(),
5794                    });
5795                }
5796                (series, data_labels, None)
5797            }
5798            Self::Doughnut {
5799                first_slice_angle,
5800                hole_size,
5801                series,
5802                data_labels,
5803            } => {
5804                if *first_slice_angle > 360 {
5805                    return Err(ChartError::InvalidValue {
5806                        element: "c:firstSliceAng".to_owned(),
5807                        value: first_slice_angle.to_string(),
5808                    });
5809                }
5810                if !(10..=90).contains(hole_size) {
5811                    return Err(ChartError::InvalidValue {
5812                        element: "c:holeSize".to_owned(),
5813                        value: hole_size.to_string(),
5814                    });
5815                }
5816                (series, data_labels, None)
5817            }
5818            Self::Area {
5819                series,
5820                data_labels,
5821                axis_ids,
5822                ..
5823            }
5824            | Self::Radar {
5825                series,
5826                data_labels,
5827                axis_ids,
5828                ..
5829            } => (series, data_labels, Some(axis_ids)),
5830            Self::Scatter {
5831                series,
5832                data_labels,
5833                axis_ids,
5834                ..
5835            } => {
5836                for item in series {
5837                    if !matches!(item.categories, Some(AxisData::Numeric(_))) {
5838                        return Err(ChartError::InvalidValue {
5839                            element: "c:xVal".to_owned(),
5840                            value: "scatter series requires numeric x values".to_owned(),
5841                        });
5842                    }
5843                }
5844                (series, data_labels, Some(axis_ids))
5845            }
5846        };
5847        if series.is_empty() {
5848            return Err(ChartError::MissingElement("c:ser".to_owned()));
5849        }
5850        let scatter_plot = matches!(self, Self::Scatter { .. });
5851        for item in series {
5852            if item.bubble_size.is_some() || item.opaque_bubble_size {
5853                return Err(ChartError::InvalidValue {
5854                    element: "c:bubbleSize".to_owned(),
5855                    value: "bubble size is only valid in an opaque bubble plot".to_owned(),
5856                });
5857            }
5858            if item.uses_scatter_wrappers && !scatter_plot {
5859                return Err(ChartError::InvalidValue {
5860                    element: "c:ser".to_owned(),
5861                    value: "x/y wrappers are only valid in scatter plots".to_owned(),
5862                });
5863            }
5864            if scatter_plot && item.parsed_from_xml && !item.uses_scatter_wrappers {
5865                return Err(ChartError::InvalidValue {
5866                    element: "c:ser".to_owned(),
5867                    value: "scatter plots require xVal/yVal wrappers".to_owned(),
5868                });
5869            }
5870        }
5871        if let Some(axis_ids) = axis_ids {
5872            if axis_ids[0] == axis_ids[1] {
5873                return Err(ChartError::DuplicateElement(format!(
5874                    "c:axId {}",
5875                    axis_ids[0].value()
5876                )));
5877            }
5878            for id in axis_ids {
5879                AxisId::new(id.value())?;
5880            }
5881        }
5882        if let Some(labels) = labels {
5883            labels.validate()?;
5884        }
5885        for item in series {
5886            item.values.validate()?;
5887        }
5888        Ok(())
5889    }
5890}
5891
5892/// The concrete ChartML root used by an axis.
5893#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5894pub enum AxisKind {
5895    Category,
5896    Value,
5897    Date,
5898    Series,
5899}
5900
5901impl AxisKind {
5902    fn parse(local: &[u8]) -> Option<Self> {
5903        match local {
5904            b"catAx" => Some(Self::Category),
5905            b"valAx" => Some(Self::Value),
5906            b"dateAx" => Some(Self::Date),
5907            b"serAx" => Some(Self::Series),
5908            _ => None,
5909        }
5910    }
5911
5912    const fn local(self) -> &'static str {
5913        match self {
5914            Self::Category => "catAx",
5915            Self::Value => "valAx",
5916            Self::Date => "dateAx",
5917            Self::Series => "serAx",
5918        }
5919    }
5920}
5921
5922/// Direction of values along an axis scale.
5923#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5924pub enum Orientation {
5925    #[default]
5926    MinMax,
5927    MaxMin,
5928}
5929
5930impl Orientation {
5931    fn parse(value: &str) -> Option<Self> {
5932        match value {
5933            "minMax" => Some(Self::MinMax),
5934            "maxMin" => Some(Self::MaxMin),
5935            _ => None,
5936        }
5937    }
5938
5939    const fn as_str(self) -> &'static str {
5940        match self {
5941            Self::MinMax => "minMax",
5942            Self::MaxMin => "maxMin",
5943        }
5944    }
5945}
5946
5947/// Which side of the plot area carries an axis.
5948#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5949pub enum AxisPosition {
5950    Bottom,
5951    Left,
5952    Right,
5953    Top,
5954}
5955
5956impl AxisPosition {
5957    fn parse(value: &str) -> Option<Self> {
5958        match value {
5959            "b" => Some(Self::Bottom),
5960            "l" => Some(Self::Left),
5961            "r" => Some(Self::Right),
5962            "t" => Some(Self::Top),
5963            _ => None,
5964        }
5965    }
5966
5967    const fn as_str(self) -> &'static str {
5968        match self {
5969            Self::Bottom => "b",
5970            Self::Left => "l",
5971            Self::Right => "r",
5972            Self::Top => "t",
5973        }
5974    }
5975}
5976
5977/// Placement of a major or minor tick relative to its axis line.
5978#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5979pub enum TickMark {
5980    Cross,
5981    Inside,
5982    #[default]
5983    None,
5984    Outside,
5985}
5986
5987impl TickMark {
5988    fn parse(value: &str) -> Option<Self> {
5989        match value {
5990            "cross" => Some(Self::Cross),
5991            "in" => Some(Self::Inside),
5992            "none" => Some(Self::None),
5993            "out" => Some(Self::Outside),
5994            _ => None,
5995        }
5996    }
5997
5998    const fn as_str(self) -> &'static str {
5999        match self {
6000            Self::Cross => "cross",
6001            Self::Inside => "in",
6002            Self::None => "none",
6003            Self::Outside => "out",
6004        }
6005    }
6006}
6007
6008/// Placement of tick labels relative to an axis.
6009#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6010pub enum TickLabelPosition {
6011    High,
6012    Low,
6013    #[default]
6014    NextTo,
6015    None,
6016}
6017
6018impl TickLabelPosition {
6019    fn parse(value: &str) -> Option<Self> {
6020        match value {
6021            "high" => Some(Self::High),
6022            "low" => Some(Self::Low),
6023            "nextTo" => Some(Self::NextTo),
6024            "none" => Some(Self::None),
6025            _ => None,
6026        }
6027    }
6028
6029    const fn as_str(self) -> &'static str {
6030        match self {
6031            Self::High => "high",
6032            Self::Low => "low",
6033            Self::NextTo => "nextTo",
6034            Self::None => "none",
6035        }
6036    }
6037}
6038
6039/// The modelled values of `c:scaling`.
6040#[derive(Clone, Debug)]
6041pub struct Scaling {
6042    pub log_base: Option<f64>,
6043    pub orientation: Orientation,
6044    pub maximum: Option<f64>,
6045    pub minimum: Option<f64>,
6046    log_base_markup: Option<ScalarMarkup>,
6047    orientation_markup: Option<ScalarMarkup>,
6048    maximum_markup: Option<ScalarMarkup>,
6049    minimum_markup: Option<ScalarMarkup>,
6050    raw_attributes: XmlAttributes,
6051    raw_children: OrderedRawChildren,
6052}
6053
6054impl PartialEq for Scaling {
6055    fn eq(&self, other: &Self) -> bool {
6056        self.log_base == other.log_base
6057            && self.orientation == other.orientation
6058            && self.maximum == other.maximum
6059            && self.minimum == other.minimum
6060            && optional_scalar_markup_eq(&self.log_base_markup, &other.log_base_markup)
6061            && optional_scalar_markup_eq(&self.orientation_markup, &other.orientation_markup)
6062            && optional_scalar_markup_eq(&self.maximum_markup, &other.maximum_markup)
6063            && optional_scalar_markup_eq(&self.minimum_markup, &other.minimum_markup)
6064            && self.raw_attributes == other.raw_attributes
6065            && self.raw_children == other.raw_children
6066    }
6067}
6068
6069impl Default for Scaling {
6070    fn default() -> Self {
6071        Self {
6072            log_base: None,
6073            orientation: Orientation::MinMax,
6074            maximum: None,
6075            minimum: None,
6076            log_base_markup: None,
6077            orientation_markup: None,
6078            maximum_markup: None,
6079            minimum_markup: None,
6080            raw_attributes: Vec::new(),
6081            raw_children: OrderedRawChildren::default(),
6082        }
6083    }
6084}
6085
6086/// Shape properties on a major or minor gridline container.
6087#[derive(Clone, Debug, Default, PartialEq)]
6088pub struct ChartLines {
6089    pub sp_pr: Option<CT_ShapeProperties>,
6090    raw_attributes: XmlAttributes,
6091    raw_children: OrderedRawChildren,
6092}
6093
6094/// Number format applied to axis values and labels.
6095#[derive(Clone, Debug, Eq, PartialEq)]
6096pub struct NumberFormat {
6097    pub format_code: String,
6098    pub source_linked: bool,
6099    raw_attributes: XmlAttributes,
6100    raw_content: Vec<Vec<u8>>,
6101}
6102
6103/// Placement of a chart data label relative to its data point.
6104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6105pub enum DataLabelPosition {
6106    BestFit,
6107    Bottom,
6108    Center,
6109    InsideBase,
6110    InsideEnd,
6111    Left,
6112    OutsideEnd,
6113    Right,
6114    Top,
6115}
6116
6117impl DataLabelPosition {
6118    fn parse(value: &str) -> Option<Self> {
6119        match value {
6120            "bestFit" => Some(Self::BestFit),
6121            "b" => Some(Self::Bottom),
6122            "ctr" => Some(Self::Center),
6123            "inBase" => Some(Self::InsideBase),
6124            "inEnd" => Some(Self::InsideEnd),
6125            "l" => Some(Self::Left),
6126            "outEnd" => Some(Self::OutsideEnd),
6127            "r" => Some(Self::Right),
6128            "t" => Some(Self::Top),
6129            _ => None,
6130        }
6131    }
6132
6133    const fn as_str(self) -> &'static str {
6134        match self {
6135            Self::BestFit => "bestFit",
6136            Self::Bottom => "b",
6137            Self::Center => "ctr",
6138            Self::InsideBase => "inBase",
6139            Self::InsideEnd => "inEnd",
6140            Self::Left => "l",
6141            Self::OutsideEnd => "outEnd",
6142            Self::Right => "r",
6143            Self::Top => "t",
6144        }
6145    }
6146}
6147
6148/// Collection-level data-label defaults for one chart series or plot.
6149#[derive(Clone, Debug, Default, PartialEq)]
6150pub struct CT_DLbls {
6151    pub number_format: Option<NumberFormat>,
6152    pub position: Option<DataLabelPosition>,
6153    pub separator: Option<String>,
6154    pub show_legend_key: bool,
6155    pub show_value: bool,
6156    pub show_category_name: bool,
6157    pub show_series_name: bool,
6158    pub show_percent: bool,
6159    pub show_bubble_size: bool,
6160    position_markup: Option<ScalarMarkup>,
6161    show_legend_key_markup: Option<ScalarMarkup>,
6162    show_value_markup: Option<ScalarMarkup>,
6163    show_category_name_markup: Option<ScalarMarkup>,
6164    show_series_name_markup: Option<ScalarMarkup>,
6165    show_percent_markup: Option<ScalarMarkup>,
6166    show_bubble_size_markup: Option<ScalarMarkup>,
6167    separator_markup: Option<TextMarkup>,
6168    raw_attributes: XmlAttributes,
6169    namespace_declarations: XmlAttributes,
6170    raw_children: OrderedRawChildren,
6171}
6172
6173/// How a chart displays cells whose cached values are blank.
6174#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
6175pub enum DispBlanksAs {
6176    #[default]
6177    Gap,
6178    Zero,
6179    Span,
6180}
6181
6182impl DispBlanksAs {
6183    fn parse(value: &str) -> Option<Self> {
6184        match value {
6185            "gap" => Some(Self::Gap),
6186            "zero" => Some(Self::Zero),
6187            "span" => Some(Self::Span),
6188            _ => None,
6189        }
6190    }
6191
6192    const fn as_str(self) -> &'static str {
6193        match self {
6194            Self::Gap => "gap",
6195            Self::Zero => "zero",
6196            Self::Span => "span",
6197        }
6198    }
6199}
6200
6201/// A chart title shell whose current children remain opaque until later stories.
6202#[derive(Clone, Debug, Default, Eq, PartialEq)]
6203pub struct CT_Title {
6204    raw_attributes: Vec<(String, String)>,
6205    raw_children: OrderedRawChildren,
6206}
6207
6208/// Preserved lexical and ordered-raw state for one typed plot.
6209#[derive(Clone, Debug, Default, PartialEq)]
6210struct PlotMarkup {
6211    raw_attributes: XmlAttributes,
6212    raw_children: OrderedRawChildren,
6213    direction: Option<ScalarMarkup>,
6214    grouping: Option<ScalarMarkup>,
6215    gap_width: Option<ScalarMarkup>,
6216    overlap: Option<ScalarMarkup>,
6217    marker: Option<ScalarMarkup>,
6218    smooth: Option<ScalarMarkup>,
6219    first_slice_angle: Option<ScalarMarkup>,
6220    hole_size: Option<ScalarMarkup>,
6221    style: Option<ScalarMarkup>,
6222    axis_ids: Vec<AxisIdMarkup>,
6223    original_series_keys: Vec<(u32, u32)>,
6224    original_axis_ids: Vec<AxisId>,
6225    parsed_bar: Option<bool>,
6226    parsed_remaining: Option<&'static str>,
6227}
6228
6229/// A plot-area shell. F-119 through F-122 replace selected raw slots with types.
6230#[derive(Clone, Debug, PartialEq)]
6231pub struct CT_PlotArea {
6232    raw_attributes: Vec<(String, String)>,
6233    raw_children: OrderedRawChildren,
6234    namespace_bindings: NamespaceBindings,
6235    plots: Option<Vec<Plot>>,
6236    plot_markup: Vec<PlotMarkup>,
6237    axes: Vec<Axis>,
6238}
6239
6240/// A chart legend shell whose current children remain opaque.
6241#[derive(Clone, Debug, Default, Eq, PartialEq)]
6242pub struct CT_Legend {
6243    raw_attributes: Vec<(String, String)>,
6244    raw_children: OrderedRawChildren,
6245}
6246
6247#[derive(Clone, Debug, PartialEq)]
6248struct AxisIdMarkup {
6249    scalar: ScalarMarkup,
6250    parsed: AxisId,
6251    lexical: String,
6252}
6253
6254/// The common modelled sequence of one ChartML axis.
6255#[derive(Clone, Debug)]
6256pub struct Axis {
6257    pub kind: AxisKind,
6258    pub id: AxisId,
6259    pub scaling: Scaling,
6260    pub deleted: bool,
6261    pub position: AxisPosition,
6262    pub major_gridlines: Option<ChartLines>,
6263    pub minor_gridlines: Option<ChartLines>,
6264    pub title: Option<CT_Title>,
6265    pub number_format: Option<NumberFormat>,
6266    pub major_tick_mark: TickMark,
6267    pub minor_tick_mark: TickMark,
6268    pub tick_label_position: TickLabelPosition,
6269    pub sp_pr: Option<CT_ShapeProperties>,
6270    pub tx_pr: Option<CT_TextBody>,
6271    pub cross_axis: AxisId,
6272    parsed_kind: Option<AxisKind>,
6273    id_markup: AxisIdMarkup,
6274    delete_markup: Option<ScalarMarkup>,
6275    position_markup: ScalarMarkup,
6276    major_tick_mark_markup: Option<ScalarMarkup>,
6277    minor_tick_mark_markup: Option<ScalarMarkup>,
6278    tick_label_position_markup: Option<ScalarMarkup>,
6279    cross_axis_markup: AxisIdMarkup,
6280    raw_attributes: XmlAttributes,
6281    namespace_declarations: XmlAttributes,
6282    raw_children: OrderedRawChildren,
6283}
6284
6285impl PartialEq for Axis {
6286    fn eq(&self, other: &Self) -> bool {
6287        self.kind == other.kind
6288            && self.id == other.id
6289            && axis_id_markup_eq(&self.id_markup, &other.id_markup)
6290            && self.scaling == other.scaling
6291            && self.deleted == other.deleted
6292            && optional_scalar_markup_eq(&self.delete_markup, &other.delete_markup)
6293            && self.position == other.position
6294            && self.position_markup == other.position_markup
6295            && self.major_gridlines == other.major_gridlines
6296            && self.minor_gridlines == other.minor_gridlines
6297            && self.title == other.title
6298            && self.number_format == other.number_format
6299            && self.major_tick_mark == other.major_tick_mark
6300            && optional_scalar_markup_eq(
6301                &self.major_tick_mark_markup,
6302                &other.major_tick_mark_markup,
6303            )
6304            && self.minor_tick_mark == other.minor_tick_mark
6305            && optional_scalar_markup_eq(
6306                &self.minor_tick_mark_markup,
6307                &other.minor_tick_mark_markup,
6308            )
6309            && self.tick_label_position == other.tick_label_position
6310            && optional_scalar_markup_eq(
6311                &self.tick_label_position_markup,
6312                &other.tick_label_position_markup,
6313            )
6314            && self.sp_pr == other.sp_pr
6315            && self.tx_pr == other.tx_pr
6316            && self.cross_axis == other.cross_axis
6317            && axis_id_markup_eq(&self.cross_axis_markup, &other.cross_axis_markup)
6318            && self.raw_attributes == other.raw_attributes
6319            && self.namespace_declarations == other.namespace_declarations
6320            && self.raw_children == other.raw_children
6321    }
6322}
6323
6324impl Axis {
6325    pub fn new(kind: AxisKind, id: AxisId, position: AxisPosition, cross_axis: AxisId) -> Self {
6326        Self {
6327            kind,
6328            id,
6329            scaling: Scaling::default(),
6330            deleted: false,
6331            position,
6332            major_gridlines: None,
6333            minor_gridlines: None,
6334            title: None,
6335            number_format: None,
6336            major_tick_mark: TickMark::None,
6337            minor_tick_mark: TickMark::None,
6338            tick_label_position: TickLabelPosition::NextTo,
6339            sp_pr: None,
6340            tx_pr: None,
6341            cross_axis,
6342            parsed_kind: None,
6343            id_markup: default_axis_id_markup(id),
6344            delete_markup: None,
6345            position_markup: ScalarMarkup::default(),
6346            major_tick_mark_markup: None,
6347            minor_tick_mark_markup: None,
6348            tick_label_position_markup: None,
6349            cross_axis_markup: default_axis_id_markup(cross_axis),
6350            raw_attributes: Vec::new(),
6351            namespace_declarations: Vec::new(),
6352            raw_children: OrderedRawChildren::default(),
6353        }
6354    }
6355
6356    pub fn from_xml(xml: &[u8]) -> Result<Self> {
6357        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
6358    }
6359
6360    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6361        let mut reader = Reader::from_reader(xml);
6362        let mut buffer = Vec::new();
6363        loop {
6364            match reader
6365                .read_event_into(&mut buffer)
6366                .map_err(OxmlError::from)?
6367            {
6368                Event::Start(element) => {
6369                    let name = element.name();
6370                    let local = local_name(name.as_ref());
6371                    let Some(kind) = AxisKind::parse(local) else {
6372                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6373                    };
6374                    chart_root_prefix(&element)?;
6375                    if !element_is_in_namespace(&element, C_NS, inherited)? {
6376                        return Err(ChartError::UnexpectedElement(element_name(&element)));
6377                    }
6378                    return Self::from_element(&mut reader, &element, inherited, kind);
6379                }
6380                Event::Empty(element) => {
6381                    let name = element.name();
6382                    let local = local_name(name.as_ref());
6383                    if AxisKind::parse(local).is_some() {
6384                        return Err(ChartError::MissingElement("c:axId".to_owned()));
6385                    }
6386                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6387                }
6388                Event::Eof => return Err(ChartError::MissingElement("ChartML axis".to_owned())),
6389                _ => {}
6390            }
6391            buffer.clear();
6392        }
6393    }
6394
6395    fn from_element(
6396        reader: &mut Reader<&[u8]>,
6397        start: &BytesStart<'_>,
6398        inherited: &NamespaceBindings,
6399        kind: AxisKind,
6400    ) -> Result<Self> {
6401        reject_conflicting_prefix(start, b"a", A_NS)?;
6402        reject_conflicting_prefix(start, b"r", R_NS)?;
6403        let namespaces = chart_bindings(inherited, start)?;
6404        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
6405        require_fixed_namespace(&namespaces, b"a", A_NS, start)?;
6406        require_fixed_namespace(&namespaces, b"r", R_NS, start)?;
6407        let (raw_attributes, _) =
6408            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
6409        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
6410        let mut state = AxisParseState::default();
6411        let mut buffer = Vec::new();
6412        loop {
6413            match reader
6414                .read_event_into(&mut buffer)
6415                .map_err(OxmlError::from)?
6416            {
6417                Event::Start(element) => {
6418                    let name = chart_child_local(&element, &namespaces)?;
6419                    let raw = capture_element(reader, &element)?;
6420                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6421                }
6422                Event::Empty(element) => {
6423                    let name = chart_child_local(&element, &namespaces)?;
6424                    let raw = capture_empty_element(&element)?;
6425                    state.parse_child(name.as_deref().unwrap_or_default(), raw, &namespaces)?;
6426                }
6427                event @ (Event::Text(_)
6428                | Event::CData(_)
6429                | Event::Comment(_)
6430                | Event::PI(_)
6431                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
6432                Event::End(element)
6433                    if matches_local_name(element.name().as_ref(), kind.local().as_bytes()) =>
6434                {
6435                    break;
6436                }
6437                Event::Eof => return Err(missing_end(&format!("c:{}", kind.local()))),
6438                _ => {}
6439            }
6440            buffer.clear();
6441        }
6442        state.finish(kind, raw_attributes, namespace_declarations)
6443    }
6444
6445    pub fn to_xml(&self) -> Result<Vec<u8>> {
6446        self.validate()?;
6447        let mut writer = Writer::new(Vec::new());
6448        let root = format!("c:{}", self.kind.local());
6449        let mut start = BytesStart::new(&root);
6450        start.push_attribute(("xmlns:c", C_NS));
6451        start.push_attribute(("xmlns:a", A_NS));
6452        start.push_attribute(("xmlns:r", R_NS));
6453        push_attributes(&mut start, &self.namespace_declarations);
6454        push_attributes(&mut start, &self.raw_attributes);
6455        writer
6456            .write_event(Event::Start(start))
6457            .map_err(OxmlError::from)?;
6458        emit_raw(&mut writer, self.raw_children.at(0))?;
6459        write_axis_id(&mut writer, "c:axId", self.id, &self.id_markup)?;
6460        emit_raw(&mut writer, self.raw_children.at(1))?;
6461        self.scaling.write_xml(&mut writer)?;
6462        emit_raw(&mut writer, self.raw_children.at(2))?;
6463        if self.deleted || self.delete_markup.is_some() {
6464            write_scalar(
6465                &mut writer,
6466                "c:delete",
6467                bool_lexical(self.deleted),
6468                self.delete_markup.as_ref(),
6469            )?;
6470        }
6471        emit_raw(&mut writer, self.raw_children.at(3))?;
6472        write_scalar(
6473            &mut writer,
6474            "c:axPos",
6475            self.position.as_str(),
6476            Some(&self.position_markup),
6477        )?;
6478        emit_raw(&mut writer, self.raw_children.at(4))?;
6479        if let Some(lines) = &self.major_gridlines {
6480            lines.write_xml(&mut writer, "c:majorGridlines")?;
6481        }
6482        emit_raw(&mut writer, self.raw_children.at(5))?;
6483        if let Some(lines) = &self.minor_gridlines {
6484            lines.write_xml(&mut writer, "c:minorGridlines")?;
6485        }
6486        emit_raw(&mut writer, self.raw_children.at(6))?;
6487        if let Some(title) = &self.title {
6488            title.write_xml(&mut writer)?;
6489        }
6490        emit_raw(&mut writer, self.raw_children.at(7))?;
6491        if let Some(number_format) = &self.number_format {
6492            number_format.write_xml(&mut writer)?;
6493        }
6494        emit_raw(&mut writer, self.raw_children.at(8))?;
6495        if self.major_tick_mark != TickMark::None || self.major_tick_mark_markup.is_some() {
6496            write_scalar(
6497                &mut writer,
6498                "c:majorTickMark",
6499                self.major_tick_mark.as_str(),
6500                self.major_tick_mark_markup.as_ref(),
6501            )?;
6502        }
6503        emit_raw(&mut writer, self.raw_children.at(9))?;
6504        if self.minor_tick_mark != TickMark::None || self.minor_tick_mark_markup.is_some() {
6505            write_scalar(
6506                &mut writer,
6507                "c:minorTickMark",
6508                self.minor_tick_mark.as_str(),
6509                self.minor_tick_mark_markup.as_ref(),
6510            )?;
6511        }
6512        emit_raw(&mut writer, self.raw_children.at(10))?;
6513        if self.tick_label_position != TickLabelPosition::NextTo
6514            || self.tick_label_position_markup.is_some()
6515        {
6516            write_scalar(
6517                &mut writer,
6518                "c:tickLblPos",
6519                self.tick_label_position.as_str(),
6520                self.tick_label_position_markup.as_ref(),
6521            )?;
6522        }
6523        emit_raw(&mut writer, self.raw_children.at(11))?;
6524        if let Some(properties) = &self.sp_pr {
6525            properties.write_xml_as(&mut writer, "c:spPr")?;
6526        }
6527        emit_raw(&mut writer, self.raw_children.at(12))?;
6528        if let Some(text) = &self.tx_pr {
6529            text.write_xml_as(&mut writer, "c:txPr")?;
6530        }
6531        emit_raw(&mut writer, self.raw_children.at(13))?;
6532        write_axis_id(
6533            &mut writer,
6534            "c:crossAx",
6535            self.cross_axis,
6536            &self.cross_axis_markup,
6537        )?;
6538        emit_raw(&mut writer, self.raw_children.at(14))?;
6539        writer
6540            .write_event(Event::End(BytesEnd::new(&root)))
6541            .map_err(OxmlError::from)?;
6542        Ok(writer.into_inner())
6543    }
6544
6545    pub fn raw_children(&self) -> &OrderedRawChildren {
6546        &self.raw_children
6547    }
6548
6549    fn validate(&self) -> Result<()> {
6550        if let Some(parsed_kind) = self.parsed_kind
6551            && self.kind != parsed_kind
6552        {
6553            return Err(ChartError::InvalidValue {
6554                element: "ChartML axis root".to_owned(),
6555                value: format!(
6556                    "parsed c:{} cannot be relabelled c:{}",
6557                    parsed_kind.local(),
6558                    self.kind.local()
6559                ),
6560            });
6561        }
6562        AxisId::new(self.id.value())?;
6563        AxisId::new(self.cross_axis.value())?;
6564        self.scaling.validate()?;
6565        if let Some(number_format) = &self.number_format {
6566            number_format.validate()?;
6567        }
6568        Ok(())
6569    }
6570}
6571
6572#[derive(Default)]
6573struct AxisParseState {
6574    id: Option<(AxisId, AxisIdMarkup)>,
6575    scaling: Option<Scaling>,
6576    deleted: Option<(bool, ScalarMarkup)>,
6577    position: Option<(AxisPosition, ScalarMarkup)>,
6578    major_gridlines: Option<ChartLines>,
6579    minor_gridlines: Option<ChartLines>,
6580    title: Option<CT_Title>,
6581    number_format: Option<NumberFormat>,
6582    major_tick_mark: Option<(TickMark, ScalarMarkup)>,
6583    minor_tick_mark: Option<(TickMark, ScalarMarkup)>,
6584    tick_label_position: Option<(TickLabelPosition, ScalarMarkup)>,
6585    sp_pr: Option<CT_ShapeProperties>,
6586    tx_pr: Option<CT_TextBody>,
6587    cross_axis: Option<(AxisId, AxisIdMarkup)>,
6588    raw_children: OrderedRawChildren,
6589    boundary: usize,
6590}
6591
6592impl AxisParseState {
6593    fn capture_event(&mut self, raw: Vec<u8>) {
6594        self.raw_children.push(self.boundary, raw);
6595    }
6596
6597    fn parse_child(
6598        &mut self,
6599        name: &[u8],
6600        raw: Vec<u8>,
6601        namespaces: &NamespaceBindings,
6602    ) -> Result<()> {
6603        match name {
6604            b"axId" => {
6605                set_once(&mut self.id, parse_axis_id_scalar(&raw, "axId")?, "c:axId")?;
6606                self.boundary = self.boundary.max(1);
6607            }
6608            b"scaling" => {
6609                set_once(
6610                    &mut self.scaling,
6611                    Scaling::from_xml(&raw, namespaces)?,
6612                    "c:scaling",
6613                )?;
6614                self.boundary = self.boundary.max(2);
6615            }
6616            b"delete" => {
6617                set_once(
6618                    &mut self.deleted,
6619                    parse_bool_value(&raw, "delete")?,
6620                    "c:delete",
6621                )?;
6622                self.boundary = self.boundary.max(3);
6623            }
6624            b"axPos" => {
6625                set_once(&mut self.position, parse_axis_position(&raw)?, "c:axPos")?;
6626                self.boundary = self.boundary.max(4);
6627            }
6628            b"majorGridlines" => {
6629                set_once(
6630                    &mut self.major_gridlines,
6631                    ChartLines::from_xml(&raw, b"majorGridlines", namespaces)?,
6632                    "c:majorGridlines",
6633                )?;
6634                self.boundary = self.boundary.max(5);
6635            }
6636            b"minorGridlines" => {
6637                set_once(
6638                    &mut self.minor_gridlines,
6639                    ChartLines::from_xml(&raw, b"minorGridlines", namespaces)?,
6640                    "c:minorGridlines",
6641                )?;
6642                self.boundary = self.boundary.max(6);
6643            }
6644            b"title" => {
6645                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
6646                self.boundary = self.boundary.max(7);
6647            }
6648            b"numFmt" => {
6649                set_once(
6650                    &mut self.number_format,
6651                    NumberFormat::from_xml(&raw)?,
6652                    "c:numFmt",
6653                )?;
6654                self.boundary = self.boundary.max(8);
6655            }
6656            b"majorTickMark" => {
6657                set_once(
6658                    &mut self.major_tick_mark,
6659                    parse_tick_mark(&raw, "majorTickMark")?,
6660                    "c:majorTickMark",
6661                )?;
6662                self.boundary = self.boundary.max(9);
6663            }
6664            b"minorTickMark" => {
6665                set_once(
6666                    &mut self.minor_tick_mark,
6667                    parse_tick_mark(&raw, "minorTickMark")?,
6668                    "c:minorTickMark",
6669                )?;
6670                self.boundary = self.boundary.max(10);
6671            }
6672            b"tickLblPos" => {
6673                set_once(
6674                    &mut self.tick_label_position,
6675                    parse_tick_label_position(&raw)?,
6676                    "c:tickLblPos",
6677                )?;
6678                self.boundary = self.boundary.max(11);
6679            }
6680            b"spPr" => {
6681                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6682                let properties = CT_ShapeProperties::from_xml(&raw)?;
6683                let mut writer = Writer::new(Vec::new());
6684                properties.write_xml_as(&mut writer, "c:spPr")?;
6685                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
6686                set_once(&mut self.sp_pr, properties, "c:spPr")?;
6687                self.boundary = self.boundary.max(12);
6688            }
6689            b"txPr" => {
6690                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
6691                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
6692                let mut writer = Writer::new(Vec::new());
6693                text.write_xml_as(&mut writer, "c:txPr")?;
6694                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
6695                set_once(&mut self.tx_pr, text, "c:txPr")?;
6696                self.boundary = self.boundary.max(13);
6697            }
6698            b"crossAx" => {
6699                set_once(
6700                    &mut self.cross_axis,
6701                    parse_axis_id_scalar(&raw, "crossAx")?,
6702                    "c:crossAx",
6703                )?;
6704                self.boundary = self.boundary.max(14);
6705            }
6706            _ => self.raw_children.push(self.boundary, raw),
6707        }
6708        Ok(())
6709    }
6710
6711    fn finish(
6712        self,
6713        kind: AxisKind,
6714        raw_attributes: XmlAttributes,
6715        namespace_declarations: XmlAttributes,
6716    ) -> Result<Axis> {
6717        let (id, id_markup) = self
6718            .id
6719            .ok_or_else(|| ChartError::MissingElement("c:axId".to_owned()))?;
6720        let scaling = self
6721            .scaling
6722            .ok_or_else(|| ChartError::MissingElement("c:scaling".to_owned()))?;
6723        let (position, position_markup) = self
6724            .position
6725            .ok_or_else(|| ChartError::MissingElement("c:axPos".to_owned()))?;
6726        let (cross_axis, cross_axis_markup) = self
6727            .cross_axis
6728            .ok_or_else(|| ChartError::MissingElement("c:crossAx".to_owned()))?;
6729        let (deleted, delete_markup) = self
6730            .deleted
6731            .map(|(value, markup)| (value, Some(markup)))
6732            .unwrap_or((false, None));
6733        let (major_tick_mark, major_tick_mark_markup) = self
6734            .major_tick_mark
6735            .map(|(value, markup)| (value, Some(markup)))
6736            .unwrap_or((TickMark::None, None));
6737        let (minor_tick_mark, minor_tick_mark_markup) = self
6738            .minor_tick_mark
6739            .map(|(value, markup)| (value, Some(markup)))
6740            .unwrap_or((TickMark::None, None));
6741        let (tick_label_position, tick_label_position_markup) = self
6742            .tick_label_position
6743            .map(|(value, markup)| (value, Some(markup)))
6744            .unwrap_or((TickLabelPosition::NextTo, None));
6745        let axis = Axis {
6746            kind,
6747            id,
6748            scaling,
6749            deleted,
6750            position,
6751            major_gridlines: self.major_gridlines,
6752            minor_gridlines: self.minor_gridlines,
6753            title: self.title,
6754            number_format: self.number_format,
6755            major_tick_mark,
6756            minor_tick_mark,
6757            tick_label_position,
6758            sp_pr: self.sp_pr,
6759            tx_pr: self.tx_pr,
6760            cross_axis,
6761            parsed_kind: Some(kind),
6762            id_markup,
6763            delete_markup,
6764            position_markup,
6765            major_tick_mark_markup,
6766            minor_tick_mark_markup,
6767            tick_label_position_markup,
6768            cross_axis_markup,
6769            raw_attributes,
6770            namespace_declarations,
6771            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
6772        };
6773        axis.validate()?;
6774        Ok(axis)
6775    }
6776}
6777
6778impl Scaling {
6779    fn from_xml(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
6780        let mut reader = Reader::from_reader(xml);
6781        let mut buffer = Vec::new();
6782        loop {
6783            match reader
6784                .read_event_into(&mut buffer)
6785                .map_err(OxmlError::from)?
6786            {
6787                Event::Start(element)
6788                    if matches_local_name(element.name().as_ref(), b"scaling") =>
6789                {
6790                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
6791                    let raw_attributes = capture_attributes(&element)?;
6792                    let mut log_base = None;
6793                    let mut orientation = None;
6794                    let mut maximum = None;
6795                    let mut minimum = None;
6796                    let mut raw_children = OrderedRawChildren::default();
6797                    let mut boundary = 0usize;
6798                    let mut inner = Vec::new();
6799                    loop {
6800                        match reader
6801                            .read_event_into(&mut inner)
6802                            .map_err(OxmlError::from)?
6803                        {
6804                            Event::Start(child) => {
6805                                let name = chart_child_local(&child, &namespaces)?;
6806                                let raw = capture_element(&mut reader, &child)?;
6807                                parse_scaling_child(
6808                                    name.as_deref().unwrap_or_default(),
6809                                    raw,
6810                                    &mut log_base,
6811                                    &mut orientation,
6812                                    &mut maximum,
6813                                    &mut minimum,
6814                                    &mut raw_children,
6815                                    &mut boundary,
6816                                )?;
6817                            }
6818                            Event::Empty(child) => {
6819                                let name = chart_child_local(&child, &namespaces)?;
6820                                let raw = capture_empty_element(&child)?;
6821                                parse_scaling_child(
6822                                    name.as_deref().unwrap_or_default(),
6823                                    raw,
6824                                    &mut log_base,
6825                                    &mut orientation,
6826                                    &mut maximum,
6827                                    &mut minimum,
6828                                    &mut raw_children,
6829                                    &mut boundary,
6830                                )?;
6831                            }
6832                            event @ (Event::Text(_)
6833                            | Event::CData(_)
6834                            | Event::Comment(_)
6835                            | Event::PI(_)
6836                            | Event::GeneralRef(_)) => {
6837                                raw_children.push(boundary, capture_event(event)?);
6838                            }
6839                            Event::End(end)
6840                                if matches_local_name(end.name().as_ref(), b"scaling") =>
6841                            {
6842                                let (log_base, log_base_markup) = optional_markup(log_base);
6843                                let (orientation, orientation_markup) = orientation
6844                                    .map(|(value, markup)| (value, Some(markup)))
6845                                    .unwrap_or((Orientation::MinMax, None));
6846                                let (maximum, maximum_markup) = optional_markup(maximum);
6847                                let (minimum, minimum_markup) = optional_markup(minimum);
6848                                let scaling = Self {
6849                                    log_base,
6850                                    orientation,
6851                                    maximum,
6852                                    minimum,
6853                                    log_base_markup,
6854                                    orientation_markup,
6855                                    maximum_markup,
6856                                    minimum_markup,
6857                                    raw_attributes,
6858                                    raw_children: raw_children_in_schema_order(&raw_children, 4),
6859                                };
6860                                scaling.validate()?;
6861                                return Ok(scaling);
6862                            }
6863                            Event::Eof => return Err(missing_end("c:scaling")),
6864                            _ => {}
6865                        }
6866                        inner.clear();
6867                    }
6868                }
6869                Event::Empty(element)
6870                    if matches_local_name(element.name().as_ref(), b"scaling") =>
6871                {
6872                    typed_rewrite_bindings(&element, inherited)?;
6873                    return Ok(Self {
6874                        raw_attributes: capture_attributes(&element)?,
6875                        ..Self::default()
6876                    });
6877                }
6878                Event::Start(element) | Event::Empty(element) => {
6879                    return Err(ChartError::UnexpectedElement(element_name(&element)));
6880                }
6881                Event::Eof => return Err(ChartError::MissingElement("c:scaling".to_owned())),
6882                _ => {}
6883            }
6884            buffer.clear();
6885        }
6886    }
6887
6888    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
6889        self.validate()?;
6890        let mut start = BytesStart::new("c:scaling");
6891        push_attributes(&mut start, &self.raw_attributes);
6892        writer
6893            .write_event(Event::Start(start))
6894            .map_err(OxmlError::from)?;
6895        emit_raw(writer, self.raw_children.at(0))?;
6896        if let Some(value) = self.log_base {
6897            write_scalar(
6898                writer,
6899                "c:logBase",
6900                &value.to_string(),
6901                self.log_base_markup.as_ref(),
6902            )?;
6903        }
6904        emit_raw(writer, self.raw_children.at(1))?;
6905        if self.orientation != Orientation::MinMax || self.orientation_markup.is_some() {
6906            write_scalar(
6907                writer,
6908                "c:orientation",
6909                self.orientation.as_str(),
6910                self.orientation_markup.as_ref(),
6911            )?;
6912        }
6913        emit_raw(writer, self.raw_children.at(2))?;
6914        if let Some(value) = self.maximum {
6915            write_scalar(
6916                writer,
6917                "c:max",
6918                &value.to_string(),
6919                self.maximum_markup.as_ref(),
6920            )?;
6921        }
6922        emit_raw(writer, self.raw_children.at(3))?;
6923        if let Some(value) = self.minimum {
6924            write_scalar(
6925                writer,
6926                "c:min",
6927                &value.to_string(),
6928                self.minimum_markup.as_ref(),
6929            )?;
6930        }
6931        emit_raw(writer, self.raw_children.at(4))?;
6932        writer
6933            .write_event(Event::End(BytesEnd::new("c:scaling")))
6934            .map_err(OxmlError::from)?;
6935        Ok(())
6936    }
6937
6938    fn validate(&self) -> Result<()> {
6939        if let Some(value) = self.log_base
6940            && (!value.is_finite() || !(2.0..=1000.0).contains(&value))
6941        {
6942            return Err(ChartError::InvalidValue {
6943                element: "c:logBase".to_owned(),
6944                value: value.to_string(),
6945            });
6946        }
6947        for (element, value) in [("c:max", self.maximum), ("c:min", self.minimum)] {
6948            if let Some(value) = value
6949                && !value.is_finite()
6950            {
6951                return Err(ChartError::InvalidValue {
6952                    element: element.to_owned(),
6953                    value: value.to_string(),
6954                });
6955            }
6956        }
6957        if let (Some(minimum), Some(maximum)) = (self.minimum, self.maximum)
6958            && minimum > maximum
6959        {
6960            return Err(ChartError::InvalidValue {
6961                element: "c:scaling".to_owned(),
6962                value: format!("minimum {minimum} exceeds maximum {maximum}"),
6963            });
6964        }
6965        Ok(())
6966    }
6967}
6968
6969#[allow(clippy::too_many_arguments)]
6970fn parse_scaling_child(
6971    name: &[u8],
6972    raw: Vec<u8>,
6973    log_base: &mut Option<(f64, ScalarMarkup)>,
6974    orientation: &mut Option<(Orientation, ScalarMarkup)>,
6975    maximum: &mut Option<(f64, ScalarMarkup)>,
6976    minimum: &mut Option<(f64, ScalarMarkup)>,
6977    raw_children: &mut OrderedRawChildren,
6978    boundary: &mut usize,
6979) -> Result<()> {
6980    match name {
6981        b"logBase" => {
6982            set_once(log_base, parse_f64_scalar(&raw, "logBase")?, "c:logBase")?;
6983            *boundary = (*boundary).max(1);
6984        }
6985        b"orientation" => {
6986            let (value, markup) = scalar_value(&raw, "orientation")?;
6987            let value = value
6988                .ok_or_else(|| invalid_attribute("orientation", "val", "<missing>".to_owned()))?;
6989            let parsed = Orientation::parse(&value)
6990                .ok_or_else(|| invalid_attribute("orientation", "val", value))?;
6991            set_once(orientation, (parsed, markup), "c:orientation")?;
6992            *boundary = (*boundary).max(2);
6993        }
6994        b"max" => {
6995            set_once(maximum, parse_f64_scalar(&raw, "max")?, "c:max")?;
6996            *boundary = (*boundary).max(3);
6997        }
6998        b"min" => {
6999            set_once(minimum, parse_f64_scalar(&raw, "min")?, "c:min")?;
7000            *boundary = (*boundary).max(4);
7001        }
7002        _ => raw_children.push(*boundary, raw),
7003    }
7004    Ok(())
7005}
7006
7007impl ChartLines {
7008    fn from_xml(xml: &[u8], local: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7009        let mut reader = Reader::from_reader(xml);
7010        let mut buffer = Vec::new();
7011        loop {
7012            match reader
7013                .read_event_into(&mut buffer)
7014                .map_err(OxmlError::from)?
7015            {
7016                Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
7017                    let namespaces = typed_rewrite_bindings(&element, inherited)?;
7018                    let raw_attributes = capture_attributes(&element)?;
7019                    let mut sp_pr = None;
7020                    let mut raw_children = OrderedRawChildren::default();
7021                    let mut boundary = 0usize;
7022                    let mut inner = Vec::new();
7023                    loop {
7024                        match reader
7025                            .read_event_into(&mut inner)
7026                            .map_err(OxmlError::from)?
7027                        {
7028                            Event::Start(child) => {
7029                                let name = chart_child_local(&child, &namespaces)?;
7030                                let raw = capture_element(&mut reader, &child)?;
7031                                if name.as_deref() == Some(b"spPr") {
7032                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7033                                    boundary = 1;
7034                                } else {
7035                                    raw_children.push(boundary, raw);
7036                                }
7037                            }
7038                            Event::Empty(child) => {
7039                                let name = chart_child_local(&child, &namespaces)?;
7040                                let raw = capture_empty_element(&child)?;
7041                                if name.as_deref() == Some(b"spPr") {
7042                                    parse_chart_lines_shape(&raw, &namespaces, &mut sp_pr)?;
7043                                    boundary = 1;
7044                                } else {
7045                                    raw_children.push(boundary, raw);
7046                                }
7047                            }
7048                            event @ (Event::Text(_)
7049                            | Event::CData(_)
7050                            | Event::Comment(_)
7051                            | Event::PI(_)
7052                            | Event::GeneralRef(_)) => {
7053                                raw_children.push(boundary, capture_event(event)?);
7054                            }
7055                            Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
7056                                return Ok(Self {
7057                                    sp_pr,
7058                                    raw_attributes,
7059                                    raw_children: raw_children_in_schema_order(&raw_children, 1),
7060                                });
7061                            }
7062                            Event::Eof => {
7063                                return Err(missing_end(&format!(
7064                                    "c:{}",
7065                                    String::from_utf8_lossy(local)
7066                                )));
7067                            }
7068                            _ => {}
7069                        }
7070                        inner.clear();
7071                    }
7072                }
7073                Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
7074                    typed_rewrite_bindings(&element, inherited)?;
7075                    return Ok(Self {
7076                        sp_pr: None,
7077                        raw_attributes: capture_attributes(&element)?,
7078                        raw_children: OrderedRawChildren::default(),
7079                    });
7080                }
7081                Event::Start(element) | Event::Empty(element) => {
7082                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7083                }
7084                Event::Eof => {
7085                    return Err(ChartError::MissingElement(format!(
7086                        "c:{}",
7087                        String::from_utf8_lossy(local)
7088                    )));
7089                }
7090                _ => {}
7091            }
7092            buffer.clear();
7093        }
7094    }
7095
7096    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
7097        let mut start = BytesStart::new(tag);
7098        push_attributes(&mut start, &self.raw_attributes);
7099        if self.sp_pr.is_none() && self.raw_children.is_empty() {
7100            writer
7101                .write_event(Event::Empty(start))
7102                .map_err(OxmlError::from)?;
7103            return Ok(());
7104        }
7105        writer
7106            .write_event(Event::Start(start))
7107            .map_err(OxmlError::from)?;
7108        emit_raw(writer, self.raw_children.at(0))?;
7109        if let Some(properties) = &self.sp_pr {
7110            properties.write_xml_as(writer, "c:spPr")?;
7111        }
7112        emit_raw(writer, self.raw_children.at(1))?;
7113        writer
7114            .write_event(Event::End(BytesEnd::new(tag)))
7115            .map_err(OxmlError::from)?;
7116        Ok(())
7117    }
7118}
7119
7120fn parse_chart_lines_shape(
7121    raw: &[u8],
7122    namespaces: &NamespaceBindings,
7123    slot: &mut Option<CT_ShapeProperties>,
7124) -> Result<()> {
7125    reject_conflicting_prefix_in_xml(raw, b"a", A_NS)?;
7126    let properties = CT_ShapeProperties::from_xml(raw)?;
7127    let mut writer = Writer::new(Vec::new());
7128    properties.write_xml_as(&mut writer, "c:spPr")?;
7129    reject_rewritten_foreign_elements(raw, &writer.into_inner(), namespaces, b"spPr")?;
7130    set_once(slot, properties, "c:spPr")
7131}
7132
7133impl NumberFormat {
7134    pub fn new(format_code: String, source_linked: bool) -> Result<Self> {
7135        let number_format = Self {
7136            format_code,
7137            source_linked,
7138            raw_attributes: Vec::new(),
7139            raw_content: Vec::new(),
7140        };
7141        number_format.validate()?;
7142        Ok(number_format)
7143    }
7144
7145    fn from_xml(xml: &[u8]) -> Result<Self> {
7146        let mut reader = Reader::from_reader(xml);
7147        let mut buffer = Vec::new();
7148        loop {
7149            match reader
7150                .read_event_into(&mut buffer)
7151                .map_err(OxmlError::from)?
7152            {
7153                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7154                    return Self::from_element(&element, Vec::new());
7155                }
7156                Event::Start(element) if matches_local_name(element.name().as_ref(), b"numFmt") => {
7157                    let mut raw_content = Vec::new();
7158                    let mut inner = Vec::new();
7159                    loop {
7160                        match reader
7161                            .read_event_into(&mut inner)
7162                            .map_err(OxmlError::from)?
7163                        {
7164                            Event::End(end)
7165                                if matches_local_name(end.name().as_ref(), b"numFmt") =>
7166                            {
7167                                return Self::from_element(&element, raw_content);
7168                            }
7169                            event @ (Event::Text(_)
7170                            | Event::CData(_)
7171                            | Event::Comment(_)
7172                            | Event::PI(_)
7173                            | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
7174                            Event::Eof => return Err(missing_end("c:numFmt")),
7175                            _ => return Err(ChartError::UnexpectedElement("c:numFmt".to_owned())),
7176                        }
7177                        inner.clear();
7178                    }
7179                }
7180                Event::Start(element) | Event::Empty(element) => {
7181                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7182                }
7183                Event::Eof => return Err(ChartError::MissingElement("c:numFmt".to_owned())),
7184                _ => {}
7185            }
7186            buffer.clear();
7187        }
7188    }
7189
7190    fn from_element(element: &BytesStart<'_>, raw_content: Vec<Vec<u8>>) -> Result<Self> {
7191        reject_conflicting_prefix(element, b"c", C_NS)?;
7192        let format_code = attribute_value(element, b"formatCode")?
7193            .ok_or_else(|| invalid_attribute("numFmt", "formatCode", "<missing>".to_owned()))?;
7194        let source_linked = attribute_value(element, b"sourceLinked")?
7195            .ok_or_else(|| invalid_attribute("numFmt", "sourceLinked", "<missing>".to_owned()))?;
7196        let source_linked = parse_bool_lexical("numFmt", "sourceLinked", &source_linked)?;
7197        let number_format = Self {
7198            format_code,
7199            source_linked,
7200            raw_attributes: capture_attributes_excluding(
7201                element,
7202                &[b"formatCode", b"sourceLinked"],
7203            )?,
7204            raw_content,
7205        };
7206        number_format.validate()?;
7207        Ok(number_format)
7208    }
7209
7210    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7211        self.validate()?;
7212        let mut start = BytesStart::new("c:numFmt");
7213        start.push_attribute(("formatCode", self.format_code.as_str()));
7214        start.push_attribute(("sourceLinked", bool_lexical(self.source_linked)));
7215        push_attributes(&mut start, &self.raw_attributes);
7216        if self.raw_content.is_empty() {
7217            writer
7218                .write_event(Event::Empty(start))
7219                .map_err(OxmlError::from)?;
7220            return Ok(());
7221        }
7222        writer
7223            .write_event(Event::Start(start))
7224            .map_err(OxmlError::from)?;
7225        for raw in &self.raw_content {
7226            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
7227        }
7228        writer
7229            .write_event(Event::End(BytesEnd::new("c:numFmt")))
7230            .map_err(OxmlError::from)?;
7231        Ok(())
7232    }
7233
7234    /// Projects one cached value through the supported deterministic subset.
7235    pub fn format_value(&self, value: f64) -> Result<String> {
7236        if !value.is_finite() {
7237            return Err(ChartError::InvalidValue {
7238                element: "c:numFmt value".to_owned(),
7239                value: value.to_string(),
7240            });
7241        }
7242        if self.format_code == "General" {
7243            return Ok(value.to_string());
7244        }
7245
7246        let (numeric_code, percentage) = self
7247            .format_code
7248            .strip_suffix('%')
7249            .map(|code| (code, true))
7250            .unwrap_or((&self.format_code, false));
7251        let precision = if numeric_code == "0" {
7252            Some(0)
7253        } else if let Some(decimals) = numeric_code.strip_prefix("0.")
7254            && !decimals.is_empty()
7255            && decimals.bytes().all(|byte| byte == b'0')
7256        {
7257            Some(decimals.len())
7258        } else {
7259            None
7260        };
7261        let Some(precision) = precision else {
7262            return Err(ChartError::InvalidValue {
7263                element: "c:numFmt/@formatCode projection".to_owned(),
7264                value: self.format_code.clone(),
7265            });
7266        };
7267        let projected = if percentage { value * 100.0 } else { value };
7268        if !projected.is_finite() {
7269            return Err(ChartError::InvalidValue {
7270                element: "c:numFmt value".to_owned(),
7271                value: projected.to_string(),
7272            });
7273        }
7274        let mut formatted = format!("{projected:.precision$}");
7275        if percentage {
7276            formatted.push('%');
7277        }
7278        Ok(formatted)
7279    }
7280
7281    fn validate(&self) -> Result<()> {
7282        if self.format_code.is_empty() {
7283            return Err(ChartError::InvalidValue {
7284                element: "c:numFmt/@formatCode".to_owned(),
7285                value: self.format_code.clone(),
7286            });
7287        }
7288        validate_xml_text(&self.format_code, "c:numFmt/@formatCode")
7289    }
7290}
7291
7292impl CT_DLbls {
7293    pub fn from_xml(xml: &[u8]) -> Result<Self> {
7294        Self::from_xml_with_namespaces(xml, &chart_namespace_defaults())
7295    }
7296
7297    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7298        let mut reader = Reader::from_reader(xml);
7299        let mut buffer = Vec::new();
7300        loop {
7301            match reader
7302                .read_event_into(&mut buffer)
7303                .map_err(OxmlError::from)?
7304            {
7305                Event::Start(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7306                    chart_root_prefix(&element)?;
7307                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7308                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7309                    }
7310                    return Self::from_element(&mut reader, &element, inherited);
7311                }
7312                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7313                    chart_root_prefix(&element)?;
7314                    let namespaces = chart_bindings(inherited, &element)?;
7315                    if !element_is_in_namespace(&element, C_NS, inherited)? {
7316                        return Err(ChartError::UnexpectedElement(element_name(&element)));
7317                    }
7318                    let (raw_attributes, _) = capture_fixed_root_attributes(
7319                        &element,
7320                        &["xmlns:c", "xmlns:a", "xmlns:r"],
7321                    )?;
7322                    return Ok(Self {
7323                        raw_attributes,
7324                        namespace_declarations: standalone_namespace_declarations(&namespaces)?,
7325                        ..Self::default()
7326                    });
7327                }
7328                Event::Start(element) | Event::Empty(element) => {
7329                    return Err(ChartError::UnexpectedElement(element_name(&element)));
7330                }
7331                Event::Eof => return Err(ChartError::MissingElement("c:dLbls".to_owned())),
7332                _ => {}
7333            }
7334            buffer.clear();
7335        }
7336    }
7337
7338    fn from_element(
7339        reader: &mut Reader<&[u8]>,
7340        start: &BytesStart<'_>,
7341        inherited: &NamespaceBindings,
7342    ) -> Result<Self> {
7343        reject_conflicting_prefix(start, b"a", A_NS)?;
7344        let namespaces = chart_bindings(inherited, start)?;
7345        require_fixed_namespace(&namespaces, b"c", C_NS, start)?;
7346        let (raw_attributes, _) =
7347            capture_fixed_root_attributes(start, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
7348        let namespace_declarations = standalone_namespace_declarations(&namespaces)?;
7349        let mut state = DataLabelsParseState::new(namespaces);
7350        let mut buffer = Vec::new();
7351        loop {
7352            match reader
7353                .read_event_into(&mut buffer)
7354                .map_err(OxmlError::from)?
7355            {
7356                Event::Start(element) => {
7357                    let name = chart_child_local(&element, &state.namespaces)?;
7358                    let raw = capture_element(reader, &element)?;
7359                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7360                }
7361                Event::Empty(element) => {
7362                    let name = chart_child_local(&element, &state.namespaces)?;
7363                    let raw = capture_empty_element(&element)?;
7364                    state.parse_child(name.as_deref().unwrap_or_default(), raw)?;
7365                }
7366                event @ (Event::Text(_)
7367                | Event::CData(_)
7368                | Event::Comment(_)
7369                | Event::PI(_)
7370                | Event::GeneralRef(_)) => state.capture_event(capture_event(event)?),
7371                Event::End(element) if matches_local_name(element.name().as_ref(), b"dLbls") => {
7372                    return state.finish(raw_attributes, namespace_declarations);
7373                }
7374                Event::Eof => return Err(missing_end("c:dLbls")),
7375                _ => {}
7376            }
7377            buffer.clear();
7378        }
7379    }
7380
7381    pub fn to_xml(&self) -> Result<Vec<u8>> {
7382        let mut writer = Writer::new(Vec::new());
7383        self.write_xml(&mut writer, true)?;
7384        Ok(writer.into_inner())
7385    }
7386
7387    fn write_xml(&self, writer: &mut Writer<Vec<u8>>, standalone: bool) -> Result<()> {
7388        self.validate()?;
7389        let mut start = BytesStart::new("c:dLbls");
7390        if standalone {
7391            start.push_attribute(("xmlns:c", C_NS));
7392            start.push_attribute(("xmlns:a", A_NS));
7393            start.push_attribute(("xmlns:r", R_NS));
7394        }
7395        push_attributes(&mut start, &self.namespace_declarations);
7396        push_attributes(&mut start, &self.raw_attributes);
7397        writer
7398            .write_event(Event::Start(start))
7399            .map_err(OxmlError::from)?;
7400        emit_raw(writer, self.raw_children.at(0))?;
7401        if let Some(number_format) = &self.number_format {
7402            number_format.write_xml(writer)?;
7403        }
7404        emit_raw(writer, self.raw_children.at(1))?;
7405        if let Some(position) = self.position {
7406            write_scalar(
7407                writer,
7408                "c:dLblPos",
7409                position.as_str(),
7410                self.position_markup.as_ref(),
7411            )?;
7412        }
7413        emit_raw(writer, self.raw_children.at(2))?;
7414        write_optional_bool(
7415            writer,
7416            "c:showLegendKey",
7417            self.show_legend_key,
7418            self.show_legend_key_markup.as_ref(),
7419        )?;
7420        emit_raw(writer, self.raw_children.at(3))?;
7421        write_optional_bool(
7422            writer,
7423            "c:showVal",
7424            self.show_value,
7425            self.show_value_markup.as_ref(),
7426        )?;
7427        emit_raw(writer, self.raw_children.at(4))?;
7428        write_optional_bool(
7429            writer,
7430            "c:showCatName",
7431            self.show_category_name,
7432            self.show_category_name_markup.as_ref(),
7433        )?;
7434        emit_raw(writer, self.raw_children.at(5))?;
7435        write_optional_bool(
7436            writer,
7437            "c:showSerName",
7438            self.show_series_name,
7439            self.show_series_name_markup.as_ref(),
7440        )?;
7441        emit_raw(writer, self.raw_children.at(6))?;
7442        write_optional_bool(
7443            writer,
7444            "c:showPercent",
7445            self.show_percent,
7446            self.show_percent_markup.as_ref(),
7447        )?;
7448        emit_raw(writer, self.raw_children.at(7))?;
7449        write_optional_bool(
7450            writer,
7451            "c:showBubbleSize",
7452            self.show_bubble_size,
7453            self.show_bubble_size_markup.as_ref(),
7454        )?;
7455        emit_raw(writer, self.raw_children.at(8))?;
7456        if let Some(separator) = &self.separator {
7457            write_text(
7458                writer,
7459                "c:separator",
7460                separator,
7461                self.separator_markup
7462                    .as_ref()
7463                    .unwrap_or(&TextMarkup::default()),
7464            )?;
7465        }
7466        emit_raw(writer, self.raw_children.at(9))?;
7467        writer
7468            .write_event(Event::End(BytesEnd::new("c:dLbls")))
7469            .map_err(OxmlError::from)?;
7470        Ok(())
7471    }
7472
7473    fn validate(&self) -> Result<()> {
7474        if let Some(number_format) = &self.number_format {
7475            number_format.validate()?;
7476        }
7477        if let Some(separator) = &self.separator {
7478            validate_xml_text(separator, "c:separator")?;
7479        }
7480        Ok(())
7481    }
7482
7483    pub fn raw_children(&self) -> &OrderedRawChildren {
7484        &self.raw_children
7485    }
7486}
7487
7488struct DataLabelsParseState {
7489    namespaces: NamespaceBindings,
7490    number_format: Option<NumberFormat>,
7491    position: Option<(DataLabelPosition, ScalarMarkup)>,
7492    show_legend_key: Option<(bool, ScalarMarkup)>,
7493    show_value: Option<(bool, ScalarMarkup)>,
7494    show_category_name: Option<(bool, ScalarMarkup)>,
7495    show_series_name: Option<(bool, ScalarMarkup)>,
7496    show_percent: Option<(bool, ScalarMarkup)>,
7497    show_bubble_size: Option<(bool, ScalarMarkup)>,
7498    separator: Option<(String, TextMarkup)>,
7499    raw_children: OrderedRawChildren,
7500    boundary: usize,
7501}
7502
7503impl DataLabelsParseState {
7504    fn new(namespaces: NamespaceBindings) -> Self {
7505        Self {
7506            namespaces,
7507            number_format: None,
7508            position: None,
7509            show_legend_key: None,
7510            show_value: None,
7511            show_category_name: None,
7512            show_series_name: None,
7513            show_percent: None,
7514            show_bubble_size: None,
7515            separator: None,
7516            raw_children: OrderedRawChildren::default(),
7517            boundary: 0,
7518        }
7519    }
7520
7521    fn capture_event(&mut self, raw: Vec<u8>) {
7522        self.raw_children.push(self.boundary, raw);
7523    }
7524
7525    fn parse_child(&mut self, name: &[u8], raw: Vec<u8>) -> Result<()> {
7526        match name {
7527            b"numFmt" => {
7528                set_once(
7529                    &mut self.number_format,
7530                    NumberFormat::from_xml(&raw)?,
7531                    "c:numFmt",
7532                )?;
7533                self.boundary = self.boundary.max(1);
7534            }
7535            b"dLblPos" => {
7536                let (value, markup) = scalar_value(&raw, "dLblPos")?;
7537                let lexical = value
7538                    .ok_or_else(|| invalid_attribute("dLblPos", "val", "<missing>".to_owned()))?;
7539                let position = DataLabelPosition::parse(&lexical)
7540                    .ok_or_else(|| invalid_attribute("dLblPos", "val", lexical))?;
7541                set_once(&mut self.position, (position, markup), "c:dLblPos")?;
7542                self.boundary = self.boundary.max(2);
7543            }
7544            b"showLegendKey" => {
7545                set_once(
7546                    &mut self.show_legend_key,
7547                    parse_bool_value(&raw, "showLegendKey")?,
7548                    "c:showLegendKey",
7549                )?;
7550                self.boundary = self.boundary.max(3);
7551            }
7552            b"showVal" => {
7553                set_once(
7554                    &mut self.show_value,
7555                    parse_bool_value(&raw, "showVal")?,
7556                    "c:showVal",
7557                )?;
7558                self.boundary = self.boundary.max(4);
7559            }
7560            b"showCatName" => {
7561                set_once(
7562                    &mut self.show_category_name,
7563                    parse_bool_value(&raw, "showCatName")?,
7564                    "c:showCatName",
7565                )?;
7566                self.boundary = self.boundary.max(5);
7567            }
7568            b"showSerName" => {
7569                set_once(
7570                    &mut self.show_series_name,
7571                    parse_bool_value(&raw, "showSerName")?,
7572                    "c:showSerName",
7573                )?;
7574                self.boundary = self.boundary.max(6);
7575            }
7576            b"showPercent" => {
7577                set_once(
7578                    &mut self.show_percent,
7579                    parse_bool_value(&raw, "showPercent")?,
7580                    "c:showPercent",
7581                )?;
7582                self.boundary = self.boundary.max(7);
7583            }
7584            b"showBubbleSize" => {
7585                set_once(
7586                    &mut self.show_bubble_size,
7587                    parse_bool_value(&raw, "showBubbleSize")?,
7588                    "c:showBubbleSize",
7589                )?;
7590                self.boundary = self.boundary.max(8);
7591            }
7592            b"separator" => {
7593                set_once(
7594                    &mut self.separator,
7595                    parse_text_element(&raw, b"separator", &self.namespaces)?,
7596                    "c:separator",
7597                )?;
7598                self.boundary = self.boundary.max(9);
7599            }
7600            _ => {
7601                let boundary = data_labels_raw_boundary(name, self.boundary);
7602                self.raw_children.push(boundary, raw);
7603                self.boundary = self.boundary.max(boundary);
7604            }
7605        }
7606        Ok(())
7607    }
7608
7609    fn finish(
7610        self,
7611        raw_attributes: XmlAttributes,
7612        namespace_declarations: XmlAttributes,
7613    ) -> Result<CT_DLbls> {
7614        let (position, position_markup) = optional_markup(self.position);
7615        let (show_legend_key, show_legend_key_markup) = optional_bool_markup(self.show_legend_key);
7616        let (show_value, show_value_markup) = optional_bool_markup(self.show_value);
7617        let (show_category_name, show_category_name_markup) =
7618            optional_bool_markup(self.show_category_name);
7619        let (show_series_name, show_series_name_markup) =
7620            optional_bool_markup(self.show_series_name);
7621        let (show_percent, show_percent_markup) = optional_bool_markup(self.show_percent);
7622        let (show_bubble_size, show_bubble_size_markup) =
7623            optional_bool_markup(self.show_bubble_size);
7624        let (separator, separator_markup) = self
7625            .separator
7626            .map(|(value, markup)| (Some(value), Some(markup)))
7627            .unwrap_or((None, None));
7628        let labels = CT_DLbls {
7629            number_format: self.number_format,
7630            position,
7631            separator,
7632            show_legend_key,
7633            show_value,
7634            show_category_name,
7635            show_series_name,
7636            show_percent,
7637            show_bubble_size,
7638            position_markup,
7639            show_legend_key_markup,
7640            show_value_markup,
7641            show_category_name_markup,
7642            show_series_name_markup,
7643            show_percent_markup,
7644            show_bubble_size_markup,
7645            separator_markup,
7646            raw_attributes,
7647            namespace_declarations,
7648            raw_children: raw_children_in_schema_order(&self.raw_children, 9),
7649        };
7650        labels.validate()?;
7651        Ok(labels)
7652    }
7653}
7654
7655fn data_labels_raw_boundary(name: &[u8], current: usize) -> usize {
7656    match name {
7657        b"dLbl" | b"delete" => 0,
7658        b"spPr" | b"txPr" => 1,
7659        b"showLeaderLines" | b"leaderLines" | b"extLst" => 9,
7660        _ => current,
7661    }
7662}
7663
7664fn optional_bool_markup(value: Option<(bool, ScalarMarkup)>) -> (bool, Option<ScalarMarkup>) {
7665    value
7666        .map(|(value, markup)| (value, Some(markup)))
7667        .unwrap_or((false, None))
7668}
7669
7670fn write_optional_bool(
7671    writer: &mut Writer<Vec<u8>>,
7672    tag: &str,
7673    value: bool,
7674    markup: Option<&ScalarMarkup>,
7675) -> Result<()> {
7676    if value || markup.is_some() {
7677        write_scalar(writer, tag, bool_lexical(value), markup)?;
7678    }
7679    Ok(())
7680}
7681
7682fn default_axis_id_markup(id: AxisId) -> AxisIdMarkup {
7683    AxisIdMarkup {
7684        scalar: ScalarMarkup::default(),
7685        parsed: id,
7686        lexical: id.value().to_string(),
7687    }
7688}
7689
7690fn parse_axis_id_scalar(xml: &[u8], local: &str) -> Result<(AxisId, AxisIdMarkup)> {
7691    let (value, scalar) = scalar_value(xml, local)?;
7692    let lexical = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7693    let parsed = lexical
7694        .parse::<i64>()
7695        .map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7696    let id = AxisId::new(parsed).map_err(|_| invalid_attribute(local, "val", lexical.clone()))?;
7697    Ok((
7698        id,
7699        AxisIdMarkup {
7700            scalar,
7701            parsed: id,
7702            lexical,
7703        },
7704    ))
7705}
7706
7707fn write_axis_id<W: Write>(
7708    writer: &mut Writer<W>,
7709    tag: &str,
7710    id: AxisId,
7711    markup: &AxisIdMarkup,
7712) -> Result<()> {
7713    AxisId::new(id.value())?;
7714    let normalized;
7715    let value = if id == markup.parsed {
7716        markup.lexical.as_str()
7717    } else {
7718        normalized = id.value().to_string();
7719        normalized.as_str()
7720    };
7721    write_scalar(writer, tag, value, Some(&markup.scalar))
7722}
7723
7724fn parse_f64_scalar(xml: &[u8], local: &str) -> Result<(f64, ScalarMarkup)> {
7725    let (value, markup) = scalar_value(xml, local)?;
7726    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7727    let parsed = value
7728        .parse::<f64>()
7729        .map_err(|_| invalid_attribute(local, "val", value.clone()))?;
7730    if !parsed.is_finite() {
7731        return Err(invalid_attribute(local, "val", value));
7732    }
7733    Ok((parsed, markup))
7734}
7735
7736fn parse_axis_position(xml: &[u8]) -> Result<(AxisPosition, ScalarMarkup)> {
7737    let (value, markup) = scalar_value(xml, "axPos")?;
7738    let value = value.ok_or_else(|| invalid_attribute("axPos", "val", "<missing>".to_owned()))?;
7739    let parsed =
7740        AxisPosition::parse(&value).ok_or_else(|| invalid_attribute("axPos", "val", value))?;
7741    Ok((parsed, markup))
7742}
7743
7744fn parse_tick_mark(xml: &[u8], local: &str) -> Result<(TickMark, ScalarMarkup)> {
7745    let (value, markup) = scalar_value(xml, local)?;
7746    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
7747    let parsed = TickMark::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
7748    Ok((parsed, markup))
7749}
7750
7751fn parse_tick_label_position(xml: &[u8]) -> Result<(TickLabelPosition, ScalarMarkup)> {
7752    let (value, markup) = scalar_value(xml, "tickLblPos")?;
7753    let value =
7754        value.ok_or_else(|| invalid_attribute("tickLblPos", "val", "<missing>".to_owned()))?;
7755    let parsed = TickLabelPosition::parse(&value)
7756        .ok_or_else(|| invalid_attribute("tickLblPos", "val", value))?;
7757    Ok((parsed, markup))
7758}
7759
7760fn optional_markup<T>(value: Option<(T, ScalarMarkup)>) -> (Option<T>, Option<ScalarMarkup>) {
7761    value
7762        .map(|(value, markup)| (Some(value), Some(markup)))
7763        .unwrap_or((None, None))
7764}
7765
7766fn optional_scalar_markup_eq(left: &Option<ScalarMarkup>, right: &Option<ScalarMarkup>) -> bool {
7767    let empty = ScalarMarkup::default();
7768    left.as_ref().unwrap_or(&empty) == right.as_ref().unwrap_or(&empty)
7769}
7770
7771fn axis_id_markup_eq(left: &AxisIdMarkup, right: &AxisIdMarkup) -> bool {
7772    left.scalar == right.scalar
7773}
7774
7775const fn bool_lexical(value: bool) -> &'static str {
7776    if value { "1" } else { "0" }
7777}
7778
7779fn parse_bool_lexical(element: &str, attribute: &str, value: &str) -> Result<bool> {
7780    match value {
7781        "1" | "true" => Ok(true),
7782        "0" | "false" => Ok(false),
7783        _ => Err(invalid_attribute(element, attribute, value.to_owned())),
7784    }
7785}
7786
7787impl CT_Title {
7788    fn from_xml(xml: &[u8]) -> Result<Self> {
7789        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"title", "c:title")?;
7790        Ok(Self {
7791            raw_attributes,
7792            raw_children,
7793        })
7794    }
7795
7796    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7797        write_raw_shell(writer, "c:title", &self.raw_attributes, &self.raw_children)
7798    }
7799
7800    pub fn raw_children(&self) -> &OrderedRawChildren {
7801        &self.raw_children
7802    }
7803}
7804
7805impl CT_PlotArea {
7806    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
7807        let (raw_attributes, captured_children) = parse_raw_shell(xml, b"plotArea", "c:plotArea")?;
7808        let namespace_bindings = root_chart_bindings(xml, b"plotArea", inherited)?;
7809        let direct_roots: Vec<_> = captured_children
7810            .at(0)
7811            .filter_map(|raw| chart_root_local(raw, &namespace_bindings).transpose())
7812            .collect::<Result<_>>()?;
7813        let plot_roots: Vec<_> = direct_roots
7814            .iter()
7815            .filter(|local| is_plot_container(local))
7816            .cloned()
7817            .collect();
7818        let typed_local = (plot_roots.len() == 1
7819            && matches!(
7820                plot_roots[0].as_slice(),
7821                b"barChart"
7822                    | b"lineChart"
7823                    | b"pieChart"
7824                    | b"doughnutChart"
7825                    | b"areaChart"
7826                    | b"scatterChart"
7827                    | b"radarChart"
7828            ))
7829        .then(|| plot_roots[0].clone());
7830
7831        let Some(typed_local) = typed_local else {
7832            return Ok(Self {
7833                raw_attributes,
7834                raw_children: captured_children,
7835                namespace_bindings,
7836                plots: None,
7837                plot_markup: Vec::new(),
7838                axes: Vec::new(),
7839            });
7840        };
7841
7842        let mut raw_children = OrderedRawChildren::default();
7843        let mut plots = Vec::new();
7844        let mut plot_markup = Vec::new();
7845        let mut axes = Vec::new();
7846        let mut boundary = 0usize;
7847        for raw in captured_children.at(0) {
7848            let local = chart_root_local(raw, &namespace_bindings)?;
7849            if local.as_deref() == Some(typed_local.as_slice()) {
7850                let (plot, markup) = parse_plot(raw, &namespace_bindings)?;
7851                plots.push(plot);
7852                plot_markup.push(markup);
7853                boundary = 1;
7854            } else if local.as_deref().and_then(AxisKind::parse).is_some() {
7855                axes.push(Axis::from_xml_with_namespaces(raw, &namespace_bindings)?);
7856                boundary = axes.len() + 1;
7857            } else {
7858                raw_children.push(boundary, raw.to_vec());
7859            }
7860        }
7861        let plot_area = Self {
7862            raw_attributes,
7863            raw_children,
7864            namespace_bindings,
7865            plots: Some(plots),
7866            plot_markup,
7867            axes,
7868        };
7869        plot_area.validate_typed()?;
7870        Ok(plot_area)
7871    }
7872
7873    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
7874        let Some(plots) = &self.plots else {
7875            return write_raw_shell(
7876                writer,
7877                "c:plotArea",
7878                &self.raw_attributes,
7879                &self.raw_children,
7880            );
7881        };
7882        self.validate_typed()?;
7883        let mut start = BytesStart::new("c:plotArea");
7884        push_attributes(&mut start, &self.raw_attributes);
7885        writer
7886            .write_event(Event::Start(start))
7887            .map_err(OxmlError::from)?;
7888        emit_raw(writer, self.raw_children.at(0))?;
7889        for (index, plot) in plots.iter().enumerate() {
7890            write_plot(
7891                writer,
7892                plot,
7893                self.plot_markup
7894                    .get(index)
7895                    .unwrap_or(&PlotMarkup::default()),
7896            )?;
7897        }
7898        emit_raw(writer, self.raw_children.at(1))?;
7899        for (index, axis) in self.axes.iter().enumerate() {
7900            writer
7901                .get_mut()
7902                .write_all(&axis.to_xml()?)
7903                .map_err(OxmlError::from)?;
7904            emit_raw(writer, self.raw_children.at(index + 2))?;
7905        }
7906        writer
7907            .write_event(Event::End(BytesEnd::new("c:plotArea")))
7908            .map_err(OxmlError::from)?;
7909        Ok(())
7910    }
7911
7912    pub fn raw_children(&self) -> &OrderedRawChildren {
7913        &self.raw_children
7914    }
7915
7916    /// Creates a supported single-family plot area with owned axes.
7917    pub fn new(plots: Vec<Plot>, axes: Vec<Axis>) -> Result<Self> {
7918        let plot_area = Self {
7919            raw_attributes: Vec::new(),
7920            raw_children: OrderedRawChildren::default(),
7921            namespace_bindings: chart_namespace_defaults(),
7922            plot_markup: vec![PlotMarkup::default(); plots.len()],
7923            plots: Some(plots),
7924            axes,
7925        };
7926        plot_area.validate_typed()?;
7927        Ok(plot_area)
7928    }
7929
7930    /// Returns the owned supported plots, or an error for an opaque choice.
7931    pub fn plots(&self) -> Result<&[Plot]> {
7932        self.plots
7933            .as_deref()
7934            .ok_or_else(|| ChartError::InvalidValue {
7935                element: "c:plotArea".to_owned(),
7936                value: "unsupported or combination plot area is opaque".to_owned(),
7937            })
7938    }
7939
7940    /// Returns mutable supported plots without exposing opaque content to edits.
7941    pub fn plots_mut(&mut self) -> Result<&mut [Plot]> {
7942        self.plots
7943            .as_deref_mut()
7944            .ok_or_else(|| ChartError::InvalidValue {
7945                element: "c:plotArea".to_owned(),
7946                value: "cannot combine typed plots with an opaque plot choice".to_owned(),
7947            })
7948    }
7949
7950    /// Parses the common series payloads nested in category-based plot shells.
7951    pub fn series(&self) -> Result<Vec<Series>> {
7952        if let Some(plots) = &self.plots {
7953            return Ok(plots
7954                .iter()
7955                .flat_map(|plot| match plot {
7956                    Plot::Bar { series, .. }
7957                    | Plot::Line { series, .. }
7958                    | Plot::Pie { series, .. }
7959                    | Plot::Doughnut { series, .. }
7960                    | Plot::Area { series, .. }
7961                    | Plot::Scatter { series, .. }
7962                    | Plot::Radar { series, .. } => series.clone(),
7963                })
7964                .collect());
7965        }
7966        let mut series = Vec::new();
7967        for raw in self.raw_children.at(0) {
7968            parse_plot_series(raw, &self.namespace_bindings, &mut series)?;
7969        }
7970        Ok(series)
7971    }
7972
7973    /// Parses and validates the direct axis children of this plot area.
7974    pub fn axes(&self) -> Result<Vec<Axis>> {
7975        if self.plots.is_some() {
7976            self.validate_typed()?;
7977            return Ok(self.axes.clone());
7978        }
7979        let mut axes = Vec::new();
7980        for raw in self.raw_children.at(0) {
7981            if let Some(axis) = parse_plot_axis(raw, &self.namespace_bindings)? {
7982                axes.push(axis);
7983            }
7984        }
7985        validate_axis_pairs(&axes)?;
7986        Ok(axes)
7987    }
7988
7989    fn validate_typed(&self) -> Result<()> {
7990        let plots = self
7991            .plots
7992            .as_ref()
7993            .ok_or_else(|| ChartError::InvalidValue {
7994                element: "c:plotArea".to_owned(),
7995                value: "opaque plot area has no typed validation view".to_owned(),
7996            })?;
7997        if plots.len() != 1 {
7998            return Err(ChartError::InvalidValue {
7999                element: "c:plotArea".to_owned(),
8000                value: "typed plot area requires exactly one plot family".to_owned(),
8001            });
8002        }
8003        validate_axis_pairs(&self.axes)?;
8004        for plot in plots {
8005            plot.validate()?;
8006            if let Some(axis_ids) = plot.axis_ids() {
8007                for id in axis_ids {
8008                    if !self.axes.iter().any(|axis| axis.id == id) {
8009                        return Err(ChartError::InvalidValue {
8010                            element: "c:axId".to_owned(),
8011                            value: format!("plot references missing axis {}", id.value()),
8012                        });
8013                    }
8014                }
8015            } else if !self.axes.is_empty() {
8016                return Err(ChartError::InvalidValue {
8017                    element: "c:plotArea".to_owned(),
8018                    value: "pie and doughnut plot areas must not own axes".to_owned(),
8019                });
8020            }
8021            if plot.axis_ids().is_some() && self.axes.len() != 2 {
8022                return Err(ChartError::InvalidValue {
8023                    element: "c:plotArea".to_owned(),
8024                    value: format!(
8025                        "axis-owned plot requires exactly 2 axes, found {}",
8026                        self.axes.len()
8027                    ),
8028                });
8029            }
8030        }
8031        Ok(())
8032    }
8033}
8034
8035impl Default for CT_PlotArea {
8036    fn default() -> Self {
8037        Self {
8038            raw_attributes: Vec::new(),
8039            raw_children: OrderedRawChildren::default(),
8040            namespace_bindings: chart_namespace_defaults(),
8041            plots: Some(Vec::new()),
8042            plot_markup: Vec::new(),
8043            axes: Vec::new(),
8044        }
8045    }
8046}
8047
8048fn chart_root_local(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Vec<u8>>> {
8049    let mut reader = Reader::from_reader(xml);
8050    let mut buffer = Vec::new();
8051    loop {
8052        match reader
8053            .read_event_into(&mut buffer)
8054            .map_err(OxmlError::from)?
8055        {
8056            Event::Start(element) | Event::Empty(element) => {
8057                return Ok(element_is_in_namespace(&element, C_NS, inherited)?
8058                    .then(|| local_name(element.name().as_ref()).to_vec()));
8059            }
8060            Event::Eof => return Ok(None),
8061            _ => {}
8062        }
8063        buffer.clear();
8064    }
8065}
8066
8067fn is_plot_container(local: &[u8]) -> bool {
8068    matches!(
8069        local,
8070        b"areaChart"
8071            | b"area3DChart"
8072            | b"barChart"
8073            | b"bar3DChart"
8074            | b"bubbleChart"
8075            | b"doughnutChart"
8076            | b"lineChart"
8077            | b"line3DChart"
8078            | b"ofPieChart"
8079            | b"pieChart"
8080            | b"pie3DChart"
8081            | b"radarChart"
8082            | b"scatterChart"
8083            | b"stockChart"
8084            | b"surfaceChart"
8085            | b"surface3DChart"
8086    )
8087}
8088
8089fn parse_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8090    let local = chart_root_local(xml, inherited)?
8091        .ok_or_else(|| ChartError::MissingElement("c:barChart or c:lineChart".to_owned()))?;
8092    match local.as_slice() {
8093        b"barChart" => parse_bar_plot(xml, inherited),
8094        b"lineChart" => parse_line_plot(xml, inherited),
8095        b"pieChart" | b"doughnutChart" | b"areaChart" | b"scatterChart" | b"radarChart" => {
8096            parse_remaining_plot(xml, inherited, &local)
8097        }
8098        _ => Err(ChartError::UnexpectedElement(format!(
8099            "c:{}",
8100            String::from_utf8_lossy(&local)
8101        ))),
8102    }
8103}
8104
8105fn parse_remaining_plot(
8106    xml: &[u8],
8107    inherited: &NamespaceBindings,
8108    local: &[u8],
8109) -> Result<(Plot, PlotMarkup)> {
8110    let root = String::from_utf8_lossy(local).into_owned();
8111    let mut reader = Reader::from_reader(xml);
8112    let mut buffer = Vec::new();
8113    loop {
8114        match reader
8115            .read_event_into(&mut buffer)
8116            .map_err(OxmlError::from)?
8117        {
8118            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
8119                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8120                let (mut raw_attributes, namespace_declarations) =
8121                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8122                raw_attributes.splice(0..0, namespace_declarations);
8123                let mut grouping = None;
8124                let mut style = None;
8125                let mut first_slice_angle = None;
8126                let mut hole_size = None;
8127                let mut series = Vec::new();
8128                let mut data_labels = None;
8129                let mut axis_ids = Vec::new();
8130                let mut raw_children = OrderedRawChildren::default();
8131                let mut boundary = 0usize;
8132                let mut inner = Vec::new();
8133                loop {
8134                    match reader
8135                        .read_event_into(&mut inner)
8136                        .map_err(OxmlError::from)?
8137                    {
8138                        Event::Start(child) => {
8139                            let name = chart_child_local(&child, &namespaces)?;
8140                            let raw = capture_element(&mut reader, &child)?;
8141                            parse_remaining_plot_child(
8142                                local,
8143                                name.as_deref().unwrap_or_default(),
8144                                raw,
8145                                &namespaces,
8146                                &mut grouping,
8147                                &mut style,
8148                                &mut first_slice_angle,
8149                                &mut hole_size,
8150                                &mut series,
8151                                &mut data_labels,
8152                                &mut axis_ids,
8153                                &mut raw_children,
8154                                &mut boundary,
8155                            )?;
8156                        }
8157                        Event::Empty(child) => {
8158                            let name = chart_child_local(&child, &namespaces)?;
8159                            let raw = capture_empty_element(&child)?;
8160                            parse_remaining_plot_child(
8161                                local,
8162                                name.as_deref().unwrap_or_default(),
8163                                raw,
8164                                &namespaces,
8165                                &mut grouping,
8166                                &mut style,
8167                                &mut first_slice_angle,
8168                                &mut hole_size,
8169                                &mut series,
8170                                &mut data_labels,
8171                                &mut axis_ids,
8172                                &mut raw_children,
8173                                &mut boundary,
8174                            )?;
8175                        }
8176                        event @ (Event::Text(_)
8177                        | Event::CData(_)
8178                        | Event::Comment(_)
8179                        | Event::PI(_)
8180                        | Event::GeneralRef(_)) => {
8181                            raw_children.push(boundary, capture_event(event)?);
8182                        }
8183                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => {
8184                            let needs_axes =
8185                                matches!(local, b"areaChart" | b"scatterChart" | b"radarChart");
8186                            if needs_axes && axis_ids.len() != 2 {
8187                                return Err(ChartError::InvalidValue {
8188                                    element: format!("c:{root}/c:axId"),
8189                                    value: format!("expected 2, found {}", axis_ids.len()),
8190                                });
8191                            }
8192                            if !needs_axes && !axis_ids.is_empty() {
8193                                return Err(ChartError::InvalidValue {
8194                                    element: format!("c:{root}/c:axId"),
8195                                    value: "axis-free plot contains axis references".to_owned(),
8196                                });
8197                            }
8198                            let parsed_ids = needs_axes.then(|| [axis_ids[0].0, axis_ids[1].0]);
8199                            let original_series_keys = series
8200                                .iter()
8201                                .map(|item| (item.index, item.order))
8202                                .collect::<Vec<_>>();
8203                            let plot = match local {
8204                                b"pieChart" => Plot::Pie {
8205                                    first_slice_angle: first_slice_angle
8206                                        .as_ref()
8207                                        .map_or(0, |item| item.0),
8208                                    series,
8209                                    data_labels,
8210                                },
8211                                b"doughnutChart" => Plot::Doughnut {
8212                                    first_slice_angle: first_slice_angle
8213                                        .as_ref()
8214                                        .map_or(0, |item| item.0),
8215                                    hole_size: hole_size.as_ref().map_or(50, |item| item.0),
8216                                    series,
8217                                    data_labels,
8218                                },
8219                                b"areaChart" => Plot::Area {
8220                                    grouping: grouping
8221                                        .as_ref()
8222                                        .ok_or_else(|| {
8223                                            ChartError::MissingElement("c:grouping".to_owned())
8224                                        })?
8225                                        .0,
8226                                    series,
8227                                    data_labels,
8228                                    axis_ids: parsed_ids.ok_or_else(|| {
8229                                        ChartError::MissingElement("c:areaChart/c:axId".to_owned())
8230                                    })?,
8231                                },
8232                                b"scatterChart" => {
8233                                    let lexical = style
8234                                        .as_ref()
8235                                        .ok_or_else(|| {
8236                                            ChartError::MissingElement("c:scatterStyle".to_owned())
8237                                        })?
8238                                        .0
8239                                        .as_str();
8240                                    Plot::Scatter {
8241                                        style: ScatterStyle::parse(lexical).ok_or_else(|| {
8242                                            invalid_attribute(
8243                                                "scatterStyle",
8244                                                "val",
8245                                                lexical.to_owned(),
8246                                            )
8247                                        })?,
8248                                        series,
8249                                        data_labels,
8250                                        axis_ids: parsed_ids.ok_or_else(|| {
8251                                            ChartError::MissingElement(
8252                                                "c:scatterChart/c:axId".to_owned(),
8253                                            )
8254                                        })?,
8255                                    }
8256                                }
8257                                b"radarChart" => {
8258                                    let lexical = style
8259                                        .as_ref()
8260                                        .ok_or_else(|| {
8261                                            ChartError::MissingElement("c:radarStyle".to_owned())
8262                                        })?
8263                                        .0
8264                                        .as_str();
8265                                    Plot::Radar {
8266                                        style: RadarStyle::parse(lexical).ok_or_else(|| {
8267                                            invalid_attribute(
8268                                                "radarStyle",
8269                                                "val",
8270                                                lexical.to_owned(),
8271                                            )
8272                                        })?,
8273                                        series,
8274                                        data_labels,
8275                                        axis_ids: parsed_ids.ok_or_else(|| {
8276                                            ChartError::MissingElement(
8277                                                "c:radarChart/c:axId".to_owned(),
8278                                            )
8279                                        })?,
8280                                    }
8281                                }
8282                                _ => {
8283                                    return Err(ChartError::UnexpectedElement(format!(
8284                                        "c:{}",
8285                                        String::from_utf8_lossy(local)
8286                                    )));
8287                                }
8288                            };
8289                            plot.validate()?;
8290                            let final_boundary = boundary.max(original_series_keys.len() + 6);
8291                            return Ok((
8292                                plot,
8293                                PlotMarkup {
8294                                    raw_attributes,
8295                                    raw_children: raw_children_in_schema_order(
8296                                        &raw_children,
8297                                        final_boundary,
8298                                    ),
8299                                    grouping: grouping.map(|item| item.1),
8300                                    first_slice_angle: first_slice_angle.map(|item| item.1),
8301                                    hole_size: hole_size.map(|item| item.1),
8302                                    style: style.map(|item| item.1),
8303                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8304                                    original_series_keys,
8305                                    original_axis_ids: parsed_ids
8306                                        .map_or_else(Vec::new, |ids| ids.to_vec()),
8307                                    parsed_remaining: match local {
8308                                        b"pieChart" => Some("pieChart"),
8309                                        b"doughnutChart" => Some("doughnutChart"),
8310                                        b"areaChart" => Some("areaChart"),
8311                                        b"scatterChart" => Some("scatterChart"),
8312                                        b"radarChart" => Some("radarChart"),
8313                                        _ => None,
8314                                    },
8315                                    ..PlotMarkup::default()
8316                                },
8317                            ));
8318                        }
8319                        Event::Eof => return Err(missing_end(&format!("c:{root}"))),
8320                        _ => {}
8321                    }
8322                    inner.clear();
8323                }
8324            }
8325            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
8326                return Err(ChartError::MissingElement("c:ser".to_owned()));
8327            }
8328            Event::Start(element) | Event::Empty(element) => {
8329                return Err(ChartError::UnexpectedElement(element_name(&element)));
8330            }
8331            Event::Eof => return Err(ChartError::MissingElement(format!("c:{root}"))),
8332            _ => {}
8333        }
8334        buffer.clear();
8335    }
8336}
8337
8338#[allow(clippy::too_many_arguments)]
8339fn parse_remaining_plot_child(
8340    root: &[u8],
8341    name: &[u8],
8342    raw: Vec<u8>,
8343    namespaces: &NamespaceBindings,
8344    grouping: &mut Option<(Grouping, ScalarMarkup)>,
8345    style: &mut Option<(String, ScalarMarkup)>,
8346    first_slice_angle: &mut Option<(u16, ScalarMarkup)>,
8347    hole_size: &mut Option<(u8, ScalarMarkup)>,
8348    series: &mut Vec<Series>,
8349    data_labels: &mut Option<CT_DLbls>,
8350    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8351    raw_children: &mut OrderedRawChildren,
8352    boundary: &mut usize,
8353) -> Result<()> {
8354    let property_first = matches!(root, b"areaChart" | b"scatterChart" | b"radarChart");
8355    let base = usize::from(property_first);
8356    match name {
8357        b"grouping" if root == b"areaChart" => {
8358            let (value, markup) = required_scalar(&raw, "grouping")?;
8359            let parsed = Grouping::parse(&value)
8360                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8361            set_once(grouping, (parsed, markup), "c:grouping")?;
8362            *boundary = (*boundary).max(1);
8363        }
8364        b"scatterStyle" if root == b"scatterChart" => {
8365            let (value, markup) = required_scalar(&raw, "scatterStyle")?;
8366            if ScatterStyle::parse(&value).is_none() {
8367                return Err(invalid_attribute("scatterStyle", "val", value));
8368            }
8369            set_once(style, (value, markup), "c:scatterStyle")?;
8370            *boundary = (*boundary).max(1);
8371        }
8372        b"radarStyle" if root == b"radarChart" => {
8373            let (value, markup) = required_scalar(&raw, "radarStyle")?;
8374            if RadarStyle::parse(&value).is_none() {
8375                return Err(invalid_attribute("radarStyle", "val", value));
8376            }
8377            set_once(style, (value, markup), "c:radarStyle")?;
8378            *boundary = (*boundary).max(1);
8379        }
8380        b"ser" => {
8381            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8382            *boundary = (*boundary).max(base + series.len());
8383        }
8384        b"dLbls" => {
8385            set_once(
8386                data_labels,
8387                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8388                "c:dLbls",
8389            )?;
8390            *boundary = (*boundary).max(base + series.len() + 1);
8391        }
8392        b"firstSliceAng" if matches!(root, b"pieChart" | b"doughnutChart") => {
8393            let (value, markup) = required_scalar(&raw, "firstSliceAng")?;
8394            let parsed = value
8395                .parse::<u16>()
8396                .map_err(|_| invalid_attribute("firstSliceAng", "val", value.clone()))?;
8397            if parsed > 360 {
8398                return Err(invalid_attribute("firstSliceAng", "val", value));
8399            }
8400            set_once(first_slice_angle, (parsed, markup), "c:firstSliceAng")?;
8401            *boundary = (*boundary).max(series.len() + 2);
8402        }
8403        b"holeSize" if root == b"doughnutChart" => {
8404            let (value, markup) = required_scalar(&raw, "holeSize")?;
8405            let parsed = value
8406                .parse::<u8>()
8407                .map_err(|_| invalid_attribute("holeSize", "val", value.clone()))?;
8408            if !(10..=90).contains(&parsed) {
8409                return Err(invalid_attribute("holeSize", "val", value));
8410            }
8411            set_once(hole_size, (parsed, markup), "c:holeSize")?;
8412            *boundary = (*boundary).max(series.len() + 3);
8413        }
8414        b"axId" if property_first => {
8415            if axis_ids.len() == 2 {
8416                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8417            }
8418            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8419            *boundary = (*boundary).max(base + series.len() + 1 + axis_ids.len());
8420        }
8421        b"axId" => {
8422            return Err(ChartError::InvalidValue {
8423                element: "c:axId".to_owned(),
8424                value: "axis-free plot contains axis reference".to_owned(),
8425            });
8426        }
8427        _ => {
8428            let raw_boundary = match (root, name) {
8429                (_, b"varyColors") => base,
8430                (b"pieChart", b"extLst") => series.len() + 2,
8431                (b"doughnutChart", b"extLst") => series.len() + 3,
8432                (b"areaChart" | b"scatterChart" | b"radarChart", b"dropLines") => {
8433                    base + series.len() + 1
8434                }
8435                (b"areaChart" | b"scatterChart" | b"radarChart", b"extLst") => {
8436                    base + series.len() + 3
8437                }
8438                _ => *boundary,
8439            };
8440            raw_children.push(raw_boundary, raw);
8441            *boundary = (*boundary).max(raw_boundary);
8442        }
8443    }
8444    Ok(())
8445}
8446
8447fn parse_bar_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8448    let mut reader = Reader::from_reader(xml);
8449    let mut buffer = Vec::new();
8450    loop {
8451        match reader
8452            .read_event_into(&mut buffer)
8453            .map_err(OxmlError::from)?
8454        {
8455            Event::Start(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8456                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8457                let (mut raw_attributes, namespace_declarations) =
8458                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8459                raw_attributes.splice(0..0, namespace_declarations);
8460                let mut direction = None;
8461                let mut grouping = None;
8462                let mut gap_width = None;
8463                let mut overlap = None;
8464                let mut series = Vec::new();
8465                let mut data_labels = None;
8466                let mut axis_ids = Vec::new();
8467                let mut raw_children = OrderedRawChildren::default();
8468                let mut boundary = 0usize;
8469                let mut inner = Vec::new();
8470                loop {
8471                    match reader
8472                        .read_event_into(&mut inner)
8473                        .map_err(OxmlError::from)?
8474                    {
8475                        Event::Start(child) => {
8476                            let name = chart_child_local(&child, &namespaces)?;
8477                            let raw = capture_element(&mut reader, &child)?;
8478                            parse_bar_child(
8479                                name.as_deref().unwrap_or_default(),
8480                                raw,
8481                                &namespaces,
8482                                &mut direction,
8483                                &mut grouping,
8484                                &mut gap_width,
8485                                &mut overlap,
8486                                &mut series,
8487                                &mut data_labels,
8488                                &mut axis_ids,
8489                                &mut raw_children,
8490                                &mut boundary,
8491                            )?;
8492                        }
8493                        Event::Empty(child) => {
8494                            let name = chart_child_local(&child, &namespaces)?;
8495                            let raw = capture_empty_element(&child)?;
8496                            parse_bar_child(
8497                                name.as_deref().unwrap_or_default(),
8498                                raw,
8499                                &namespaces,
8500                                &mut direction,
8501                                &mut grouping,
8502                                &mut gap_width,
8503                                &mut overlap,
8504                                &mut series,
8505                                &mut data_labels,
8506                                &mut axis_ids,
8507                                &mut raw_children,
8508                                &mut boundary,
8509                            )?;
8510                        }
8511                        event @ (Event::Text(_)
8512                        | Event::CData(_)
8513                        | Event::Comment(_)
8514                        | Event::PI(_)
8515                        | Event::GeneralRef(_)) => {
8516                            raw_children.push(boundary, capture_event(event)?);
8517                        }
8518                        Event::End(end) if matches_local_name(end.name().as_ref(), b"barChart") => {
8519                            let (direction, direction_markup) = direction
8520                                .ok_or_else(|| ChartError::MissingElement("c:barDir".to_owned()))?;
8521                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8522                                ChartError::MissingElement("c:grouping".to_owned())
8523                            })?;
8524                            if axis_ids.len() != 2 {
8525                                return Err(ChartError::InvalidValue {
8526                                    element: "c:barChart/c:axId".to_owned(),
8527                                    value: format!("expected 2, found {}", axis_ids.len()),
8528                                });
8529                            }
8530                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8531                            let original_series_keys: Vec<(u32, u32)> =
8532                                series.iter().map(|item| (item.index, item.order)).collect();
8533                            let plot = Plot::Bar {
8534                                direction,
8535                                grouping,
8536                                gap_width: gap_width.as_ref().map_or(150, |item| item.0),
8537                                overlap: overlap.as_ref().map_or(0, |item| item.0),
8538                                series,
8539                                data_labels,
8540                                axis_ids: parsed_ids,
8541                            };
8542                            plot.validate()?;
8543                            return Ok((
8544                                plot,
8545                                PlotMarkup {
8546                                    raw_attributes,
8547                                    raw_children: raw_children_in_schema_order(
8548                                        &raw_children,
8549                                        original_series_keys.len() + 7,
8550                                    ),
8551                                    direction: Some(direction_markup),
8552                                    grouping: Some(grouping_markup),
8553                                    gap_width: gap_width.map(|item| item.1),
8554                                    overlap: overlap.map(|item| item.1),
8555                                    marker: None,
8556                                    smooth: None,
8557                                    first_slice_angle: None,
8558                                    hole_size: None,
8559                                    style: None,
8560                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8561                                    original_series_keys,
8562                                    original_axis_ids: parsed_ids.to_vec(),
8563                                    parsed_bar: Some(true),
8564                                    parsed_remaining: None,
8565                                },
8566                            ));
8567                        }
8568                        Event::Eof => return Err(missing_end("c:barChart")),
8569                        _ => {}
8570                    }
8571                    inner.clear();
8572                }
8573            }
8574            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"barChart") => {
8575                return Err(ChartError::MissingElement("c:barDir".to_owned()));
8576            }
8577            Event::Start(element) | Event::Empty(element) => {
8578                return Err(ChartError::UnexpectedElement(element_name(&element)));
8579            }
8580            Event::Eof => return Err(ChartError::MissingElement("c:barChart".to_owned())),
8581            _ => {}
8582        }
8583        buffer.clear();
8584    }
8585}
8586
8587#[allow(clippy::too_many_arguments)]
8588fn parse_bar_child(
8589    name: &[u8],
8590    raw: Vec<u8>,
8591    namespaces: &NamespaceBindings,
8592    direction: &mut Option<(BarDirection, ScalarMarkup)>,
8593    grouping: &mut Option<(BarGrouping, ScalarMarkup)>,
8594    gap_width: &mut Option<(u16, ScalarMarkup)>,
8595    overlap: &mut Option<(i8, ScalarMarkup)>,
8596    series: &mut Vec<Series>,
8597    data_labels: &mut Option<CT_DLbls>,
8598    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8599    raw_children: &mut OrderedRawChildren,
8600    boundary: &mut usize,
8601) -> Result<()> {
8602    match name {
8603        b"barDir" => {
8604            let (value, markup) = required_scalar(&raw, "barDir")?;
8605            let value = BarDirection::parse(&value)
8606                .ok_or_else(|| invalid_attribute("barDir", "val", value))?;
8607            set_once(direction, (value, markup), "c:barDir")?;
8608            *boundary = (*boundary).max(1);
8609        }
8610        b"grouping" => {
8611            let (value, markup) = required_scalar(&raw, "grouping")?;
8612            let value = BarGrouping::parse(&value)
8613                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8614            set_once(grouping, (value, markup), "c:grouping")?;
8615            *boundary = (*boundary).max(2);
8616        }
8617        b"ser" => {
8618            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8619            *boundary = (*boundary).max(series.len() + 2);
8620        }
8621        b"dLbls" => {
8622            set_once(
8623                data_labels,
8624                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8625                "c:dLbls",
8626            )?;
8627            *boundary = (*boundary).max(series.len() + 3);
8628        }
8629        b"gapWidth" => {
8630            let (value, markup) = required_scalar(&raw, "gapWidth")?;
8631            let parsed = value
8632                .parse::<u16>()
8633                .map_err(|_| invalid_attribute("gapWidth", "val", value.clone()))?;
8634            if parsed > 500 {
8635                return Err(invalid_attribute("gapWidth", "val", value));
8636            }
8637            set_once(gap_width, (parsed, markup), "c:gapWidth")?;
8638            *boundary = (*boundary).max(series.len() + 4);
8639        }
8640        b"overlap" => {
8641            let (value, markup) = required_scalar(&raw, "overlap")?;
8642            let parsed = value
8643                .parse::<i8>()
8644                .map_err(|_| invalid_attribute("overlap", "val", value.clone()))?;
8645            if !(-100..=100).contains(&parsed) {
8646                return Err(invalid_attribute("overlap", "val", value));
8647            }
8648            set_once(overlap, (parsed, markup), "c:overlap")?;
8649            *boundary = (*boundary).max(series.len() + 5);
8650        }
8651        b"axId" => {
8652            if axis_ids.len() == 2 {
8653                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8654            }
8655            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8656            *boundary = (*boundary).max(series.len() + 5 + axis_ids.len());
8657        }
8658        _ => raw_children.push(*boundary, raw),
8659    }
8660    Ok(())
8661}
8662
8663fn parse_line_plot(xml: &[u8], inherited: &NamespaceBindings) -> Result<(Plot, PlotMarkup)> {
8664    let mut reader = Reader::from_reader(xml);
8665    let mut buffer = Vec::new();
8666    loop {
8667        match reader
8668            .read_event_into(&mut buffer)
8669            .map_err(OxmlError::from)?
8670        {
8671            Event::Start(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
8672                let namespaces = typed_rewrite_bindings(&element, inherited)?;
8673                let (mut raw_attributes, namespace_declarations) =
8674                    capture_fixed_root_attributes(&element, &["xmlns:c", "xmlns:a", "xmlns:r"])?;
8675                raw_attributes.splice(0..0, namespace_declarations);
8676                let mut grouping = None;
8677                let mut marker = None;
8678                let mut smooth = None;
8679                let mut series = Vec::new();
8680                let mut data_labels = None;
8681                let mut axis_ids = Vec::new();
8682                let mut raw_children = OrderedRawChildren::default();
8683                let mut boundary = 0usize;
8684                let mut inner = Vec::new();
8685                loop {
8686                    match reader
8687                        .read_event_into(&mut inner)
8688                        .map_err(OxmlError::from)?
8689                    {
8690                        Event::Start(child) => {
8691                            let name = chart_child_local(&child, &namespaces)?;
8692                            let raw = capture_element(&mut reader, &child)?;
8693                            parse_line_child(
8694                                name.as_deref().unwrap_or_default(),
8695                                raw,
8696                                &namespaces,
8697                                &mut grouping,
8698                                &mut marker,
8699                                &mut smooth,
8700                                &mut series,
8701                                &mut data_labels,
8702                                &mut axis_ids,
8703                                &mut raw_children,
8704                                &mut boundary,
8705                            )?;
8706                        }
8707                        Event::Empty(child) => {
8708                            let name = chart_child_local(&child, &namespaces)?;
8709                            let raw = capture_empty_element(&child)?;
8710                            parse_line_child(
8711                                name.as_deref().unwrap_or_default(),
8712                                raw,
8713                                &namespaces,
8714                                &mut grouping,
8715                                &mut marker,
8716                                &mut smooth,
8717                                &mut series,
8718                                &mut data_labels,
8719                                &mut axis_ids,
8720                                &mut raw_children,
8721                                &mut boundary,
8722                            )?;
8723                        }
8724                        event @ (Event::Text(_)
8725                        | Event::CData(_)
8726                        | Event::Comment(_)
8727                        | Event::PI(_)
8728                        | Event::GeneralRef(_)) => {
8729                            raw_children.push(boundary, capture_event(event)?);
8730                        }
8731                        Event::End(end)
8732                            if matches_local_name(end.name().as_ref(), b"lineChart") =>
8733                        {
8734                            let (grouping, grouping_markup) = grouping.ok_or_else(|| {
8735                                ChartError::MissingElement("c:grouping".to_owned())
8736                            })?;
8737                            if axis_ids.len() != 2 {
8738                                return Err(ChartError::InvalidValue {
8739                                    element: "c:lineChart/c:axId".to_owned(),
8740                                    value: format!("expected 2, found {}", axis_ids.len()),
8741                                });
8742                            }
8743                            let parsed_ids = [axis_ids[0].0, axis_ids[1].0];
8744                            let original_series_keys: Vec<(u32, u32)> =
8745                                series.iter().map(|item| (item.index, item.order)).collect();
8746                            let plot = Plot::Line {
8747                                grouping,
8748                                marker: marker.as_ref().is_some_and(|item| item.0),
8749                                smooth: smooth.as_ref().is_some_and(|item| item.0),
8750                                series,
8751                                data_labels,
8752                                axis_ids: parsed_ids,
8753                            };
8754                            plot.validate()?;
8755                            return Ok((
8756                                plot,
8757                                PlotMarkup {
8758                                    raw_attributes,
8759                                    raw_children: raw_children_in_schema_order(
8760                                        &raw_children,
8761                                        original_series_keys.len() + 6,
8762                                    ),
8763                                    direction: None,
8764                                    grouping: Some(grouping_markup),
8765                                    gap_width: None,
8766                                    overlap: None,
8767                                    marker: marker.map(|item| item.1),
8768                                    smooth: smooth.map(|item| item.1),
8769                                    first_slice_angle: None,
8770                                    hole_size: None,
8771                                    style: None,
8772                                    axis_ids: axis_ids.into_iter().map(|item| item.1).collect(),
8773                                    original_series_keys,
8774                                    original_axis_ids: parsed_ids.to_vec(),
8775                                    parsed_bar: Some(false),
8776                                    parsed_remaining: None,
8777                                },
8778                            ));
8779                        }
8780                        Event::Eof => return Err(missing_end("c:lineChart")),
8781                        _ => {}
8782                    }
8783                    inner.clear();
8784                }
8785            }
8786            Event::Empty(element) if matches_local_name(element.name().as_ref(), b"lineChart") => {
8787                return Err(ChartError::MissingElement("c:grouping".to_owned()));
8788            }
8789            Event::Start(element) | Event::Empty(element) => {
8790                return Err(ChartError::UnexpectedElement(element_name(&element)));
8791            }
8792            Event::Eof => return Err(ChartError::MissingElement("c:lineChart".to_owned())),
8793            _ => {}
8794        }
8795        buffer.clear();
8796    }
8797}
8798
8799#[allow(clippy::too_many_arguments)]
8800fn parse_line_child(
8801    name: &[u8],
8802    raw: Vec<u8>,
8803    namespaces: &NamespaceBindings,
8804    grouping: &mut Option<(Grouping, ScalarMarkup)>,
8805    marker: &mut Option<(bool, ScalarMarkup)>,
8806    smooth: &mut Option<(bool, ScalarMarkup)>,
8807    series: &mut Vec<Series>,
8808    data_labels: &mut Option<CT_DLbls>,
8809    axis_ids: &mut Vec<(AxisId, AxisIdMarkup)>,
8810    raw_children: &mut OrderedRawChildren,
8811    boundary: &mut usize,
8812) -> Result<()> {
8813    match name {
8814        b"grouping" => {
8815            let (value, markup) = required_scalar(&raw, "grouping")?;
8816            let value = Grouping::parse(&value)
8817                .ok_or_else(|| invalid_attribute("grouping", "val", value))?;
8818            set_once(grouping, (value, markup), "c:grouping")?;
8819            *boundary = (*boundary).max(1);
8820        }
8821        b"ser" => {
8822            series.push(Series::from_xml_with_namespaces(&raw, namespaces)?);
8823            *boundary = (*boundary).max(series.len() + 1);
8824        }
8825        b"dLbls" => {
8826            set_once(
8827                data_labels,
8828                CT_DLbls::from_xml_with_namespaces(&raw, namespaces)?,
8829                "c:dLbls",
8830            )?;
8831            *boundary = (*boundary).max(series.len() + 2);
8832        }
8833        b"marker" => {
8834            set_once(marker, parse_bool_value(&raw, "marker")?, "c:marker")?;
8835            *boundary = (*boundary).max(series.len() + 3);
8836        }
8837        b"smooth" => {
8838            set_once(smooth, parse_bool_value(&raw, "smooth")?, "c:smooth")?;
8839            *boundary = (*boundary).max(series.len() + 4);
8840        }
8841        b"axId" => {
8842            if axis_ids.len() == 2 {
8843                return Err(ChartError::DuplicateElement("c:axId".to_owned()));
8844            }
8845            axis_ids.push(parse_axis_id_scalar(&raw, "axId")?);
8846            *boundary = (*boundary).max(series.len() + 4 + axis_ids.len());
8847        }
8848        _ => raw_children.push(*boundary, raw),
8849    }
8850    Ok(())
8851}
8852
8853fn required_scalar(xml: &[u8], local: &str) -> Result<(String, ScalarMarkup)> {
8854    let (value, markup) = scalar_value(xml, local)?;
8855    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
8856    Ok((value, markup))
8857}
8858
8859fn write_plot<W: Write>(writer: &mut Writer<W>, plot: &Plot, markup: &PlotMarkup) -> Result<()> {
8860    plot.validate()?;
8861    if let Some(parsed_bar) = markup.parsed_bar
8862        && parsed_bar != matches!(plot, Plot::Bar { .. })
8863    {
8864        return Err(ChartError::InvalidValue {
8865            element: "c:plotArea".to_owned(),
8866            value: "a parsed plot family cannot be replaced while preserved payload remains"
8867                .to_owned(),
8868        });
8869    }
8870    let current_remaining = match plot {
8871        Plot::Pie { .. } => Some("pieChart"),
8872        Plot::Doughnut { .. } => Some("doughnutChart"),
8873        Plot::Area { .. } => Some("areaChart"),
8874        Plot::Scatter { .. } => Some("scatterChart"),
8875        Plot::Radar { .. } => Some("radarChart"),
8876        Plot::Bar { .. } | Plot::Line { .. } => None,
8877    };
8878    if let Some(parsed) = markup.parsed_remaining
8879        && current_remaining != Some(parsed)
8880    {
8881        return Err(ChartError::InvalidValue {
8882            element: "c:plotArea".to_owned(),
8883            value: "a parsed plot family cannot be replaced while preserved payload remains"
8884                .to_owned(),
8885        });
8886    }
8887    match plot {
8888        Plot::Bar {
8889            direction,
8890            grouping,
8891            gap_width,
8892            overlap,
8893            series,
8894            data_labels,
8895            axis_ids,
8896        } => {
8897            let mut start = BytesStart::new("c:barChart");
8898            push_attributes(&mut start, &markup.raw_attributes);
8899            writer
8900                .write_event(Event::Start(start))
8901                .map_err(OxmlError::from)?;
8902            emit_raw(writer, markup.raw_children.at(0))?;
8903            write_scalar(
8904                writer,
8905                "c:barDir",
8906                direction.as_str(),
8907                markup.direction.as_ref(),
8908            )?;
8909            emit_raw(writer, markup.raw_children.at(1))?;
8910            write_scalar(
8911                writer,
8912                "c:grouping",
8913                grouping.as_str(),
8914                markup.grouping.as_ref(),
8915            )?;
8916            let original_to_current =
8917                series_original_to_current(&markup.original_series_keys, series);
8918            emit_repeated_raw(
8919                writer,
8920                &markup.raw_children,
8921                2,
8922                &original_to_current,
8923                series.len(),
8924                0,
8925            )?;
8926            for (index, item) in series.iter().enumerate() {
8927                writer
8928                    .get_mut()
8929                    .write_all(&item.to_xml()?)
8930                    .map_err(OxmlError::from)?;
8931                emit_repeated_raw(
8932                    writer,
8933                    &markup.raw_children,
8934                    2,
8935                    &original_to_current,
8936                    series.len(),
8937                    index + 1,
8938                )?;
8939            }
8940            if let Some(labels) = data_labels {
8941                writer
8942                    .get_mut()
8943                    .write_all(&labels.to_xml()?)
8944                    .map_err(OxmlError::from)?;
8945            }
8946            let trailing = markup.original_series_keys.len();
8947            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
8948            if *gap_width != 150 || markup.gap_width.is_some() {
8949                write_scalar(
8950                    writer,
8951                    "c:gapWidth",
8952                    &gap_width.to_string(),
8953                    markup.gap_width.as_ref(),
8954                )?;
8955            }
8956            emit_raw(writer, markup.raw_children.at(trailing + 4))?;
8957            if *overlap != 0 || markup.overlap.is_some() {
8958                write_scalar(
8959                    writer,
8960                    "c:overlap",
8961                    &overlap.to_string(),
8962                    markup.overlap.as_ref(),
8963                )?;
8964            }
8965            let axis_original_to_current =
8966                axis_original_to_current(&markup.original_axis_ids, axis_ids);
8967            let axis_current_to_original =
8968                invert_original_to_current(&axis_original_to_current, axis_ids.len());
8969            emit_repeated_raw(
8970                writer,
8971                &markup.raw_children,
8972                trailing + 5,
8973                &axis_original_to_current,
8974                axis_ids.len(),
8975                0,
8976            )?;
8977            for (index, id) in axis_ids.iter().enumerate() {
8978                let default_markup = default_axis_id_markup(*id);
8979                let original_markup = axis_current_to_original[index]
8980                    .and_then(|original| markup.axis_ids.get(original));
8981                write_axis_id(
8982                    writer,
8983                    "c:axId",
8984                    *id,
8985                    original_markup.unwrap_or(&default_markup),
8986                )?;
8987                emit_repeated_raw(
8988                    writer,
8989                    &markup.raw_children,
8990                    trailing + 5,
8991                    &axis_original_to_current,
8992                    axis_ids.len(),
8993                    index + 1,
8994                )?;
8995            }
8996            writer
8997                .write_event(Event::End(BytesEnd::new("c:barChart")))
8998                .map_err(OxmlError::from)?;
8999        }
9000        Plot::Line {
9001            grouping,
9002            marker,
9003            smooth,
9004            series,
9005            data_labels,
9006            axis_ids,
9007        } => {
9008            let mut start = BytesStart::new("c:lineChart");
9009            push_attributes(&mut start, &markup.raw_attributes);
9010            writer
9011                .write_event(Event::Start(start))
9012                .map_err(OxmlError::from)?;
9013            emit_raw(writer, markup.raw_children.at(0))?;
9014            write_scalar(
9015                writer,
9016                "c:grouping",
9017                grouping.as_str(),
9018                markup.grouping.as_ref(),
9019            )?;
9020            let original_to_current =
9021                series_original_to_current(&markup.original_series_keys, series);
9022            emit_repeated_raw(
9023                writer,
9024                &markup.raw_children,
9025                1,
9026                &original_to_current,
9027                series.len(),
9028                0,
9029            )?;
9030            for (index, item) in series.iter().enumerate() {
9031                writer
9032                    .get_mut()
9033                    .write_all(&item.to_xml()?)
9034                    .map_err(OxmlError::from)?;
9035                emit_repeated_raw(
9036                    writer,
9037                    &markup.raw_children,
9038                    1,
9039                    &original_to_current,
9040                    series.len(),
9041                    index + 1,
9042                )?;
9043            }
9044            if let Some(labels) = data_labels {
9045                writer
9046                    .get_mut()
9047                    .write_all(&labels.to_xml()?)
9048                    .map_err(OxmlError::from)?;
9049            }
9050            let trailing = markup.original_series_keys.len();
9051            emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9052            if *marker || markup.marker.is_some() {
9053                write_scalar(
9054                    writer,
9055                    "c:marker",
9056                    bool_lexical(*marker),
9057                    markup.marker.as_ref(),
9058                )?;
9059            }
9060            emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9061            if *smooth || markup.smooth.is_some() {
9062                write_scalar(
9063                    writer,
9064                    "c:smooth",
9065                    bool_lexical(*smooth),
9066                    markup.smooth.as_ref(),
9067                )?;
9068            }
9069            let axis_original_to_current =
9070                axis_original_to_current(&markup.original_axis_ids, axis_ids);
9071            let axis_current_to_original =
9072                invert_original_to_current(&axis_original_to_current, axis_ids.len());
9073            emit_repeated_raw(
9074                writer,
9075                &markup.raw_children,
9076                trailing + 4,
9077                &axis_original_to_current,
9078                axis_ids.len(),
9079                0,
9080            )?;
9081            for (index, id) in axis_ids.iter().enumerate() {
9082                let default_markup = default_axis_id_markup(*id);
9083                let original_markup = axis_current_to_original[index]
9084                    .and_then(|original| markup.axis_ids.get(original));
9085                write_axis_id(
9086                    writer,
9087                    "c:axId",
9088                    *id,
9089                    original_markup.unwrap_or(&default_markup),
9090                )?;
9091                emit_repeated_raw(
9092                    writer,
9093                    &markup.raw_children,
9094                    trailing + 4,
9095                    &axis_original_to_current,
9096                    axis_ids.len(),
9097                    index + 1,
9098                )?;
9099            }
9100            writer
9101                .write_event(Event::End(BytesEnd::new("c:lineChart")))
9102                .map_err(OxmlError::from)?;
9103        }
9104        remaining => {
9105            let (
9106                root,
9107                property,
9108                series,
9109                data_labels,
9110                axis_ids,
9111                first_slice_angle,
9112                hole_size,
9113                scatter,
9114            ) = match remaining {
9115                Plot::Pie {
9116                    first_slice_angle,
9117                    series,
9118                    data_labels,
9119                } => (
9120                    "pieChart",
9121                    None,
9122                    series,
9123                    data_labels,
9124                    None,
9125                    Some(*first_slice_angle),
9126                    None,
9127                    false,
9128                ),
9129                Plot::Doughnut {
9130                    first_slice_angle,
9131                    hole_size,
9132                    series,
9133                    data_labels,
9134                } => (
9135                    "doughnutChart",
9136                    None,
9137                    series,
9138                    data_labels,
9139                    None,
9140                    Some(*first_slice_angle),
9141                    Some(*hole_size),
9142                    false,
9143                ),
9144                Plot::Area {
9145                    grouping,
9146                    series,
9147                    data_labels,
9148                    axis_ids,
9149                } => (
9150                    "areaChart",
9151                    Some(("c:grouping", grouping.as_str(), markup.grouping.as_ref())),
9152                    series,
9153                    data_labels,
9154                    Some(axis_ids),
9155                    None,
9156                    None,
9157                    false,
9158                ),
9159                Plot::Scatter {
9160                    style,
9161                    series,
9162                    data_labels,
9163                    axis_ids,
9164                } => (
9165                    "scatterChart",
9166                    Some(("c:scatterStyle", style.as_str(), markup.style.as_ref())),
9167                    series,
9168                    data_labels,
9169                    Some(axis_ids),
9170                    None,
9171                    None,
9172                    true,
9173                ),
9174                Plot::Radar {
9175                    style,
9176                    series,
9177                    data_labels,
9178                    axis_ids,
9179                } => (
9180                    "radarChart",
9181                    Some(("c:radarStyle", style.as_str(), markup.style.as_ref())),
9182                    series,
9183                    data_labels,
9184                    Some(axis_ids),
9185                    None,
9186                    None,
9187                    false,
9188                ),
9189                Plot::Bar { .. } | Plot::Line { .. } => unreachable!("handled above"),
9190            };
9191            let tag = format!("c:{root}");
9192            let mut start = BytesStart::new(&tag);
9193            push_attributes(&mut start, &markup.raw_attributes);
9194            writer
9195                .write_event(Event::Start(start))
9196                .map_err(OxmlError::from)?;
9197            let base = usize::from(property.is_some());
9198            if let Some((name, value, scalar_markup)) = property {
9199                emit_raw(writer, markup.raw_children.at(0))?;
9200                write_scalar(writer, name, value, scalar_markup)?;
9201            }
9202            let original_to_current =
9203                series_original_to_current(&markup.original_series_keys, series);
9204            emit_repeated_raw(
9205                writer,
9206                &markup.raw_children,
9207                base,
9208                &original_to_current,
9209                series.len(),
9210                0,
9211            )?;
9212            for (index, item) in series.iter().enumerate() {
9213                writer
9214                    .get_mut()
9215                    .write_all(&item.to_xml_for_plot(scatter)?)
9216                    .map_err(OxmlError::from)?;
9217                emit_repeated_raw(
9218                    writer,
9219                    &markup.raw_children,
9220                    base,
9221                    &original_to_current,
9222                    series.len(),
9223                    index + 1,
9224                )?;
9225            }
9226            if let Some(labels) = data_labels {
9227                writer
9228                    .get_mut()
9229                    .write_all(&labels.to_xml()?)
9230                    .map_err(OxmlError::from)?;
9231            }
9232            let trailing = base + markup.original_series_keys.len();
9233            if let Some(axis_ids) = axis_ids {
9234                let axis_original_to_current =
9235                    axis_original_to_current(&markup.original_axis_ids, axis_ids);
9236                let axis_current_to_original =
9237                    invert_original_to_current(&axis_original_to_current, axis_ids.len());
9238                emit_repeated_raw(
9239                    writer,
9240                    &markup.raw_children,
9241                    trailing + 1,
9242                    &axis_original_to_current,
9243                    axis_ids.len(),
9244                    0,
9245                )?;
9246                for (index, id) in axis_ids.iter().enumerate() {
9247                    let default_markup = default_axis_id_markup(*id);
9248                    let original_markup = axis_current_to_original[index]
9249                        .and_then(|original| markup.axis_ids.get(original));
9250                    write_axis_id(
9251                        writer,
9252                        "c:axId",
9253                        *id,
9254                        original_markup.unwrap_or(&default_markup),
9255                    )?;
9256                    emit_repeated_raw(
9257                        writer,
9258                        &markup.raw_children,
9259                        trailing + 1,
9260                        &axis_original_to_current,
9261                        axis_ids.len(),
9262                        index + 1,
9263                    )?;
9264                }
9265            } else {
9266                emit_raw(writer, markup.raw_children.at(trailing + 1))?;
9267                if let Some(angle) = first_slice_angle {
9268                    if angle != 0 || markup.first_slice_angle.is_some() {
9269                        write_scalar(
9270                            writer,
9271                            "c:firstSliceAng",
9272                            &angle.to_string(),
9273                            markup.first_slice_angle.as_ref(),
9274                        )?;
9275                    }
9276                    emit_raw(writer, markup.raw_children.at(trailing + 2))?;
9277                }
9278                if let Some(size) = hole_size {
9279                    if size != 50 || markup.hole_size.is_some() {
9280                        write_scalar(
9281                            writer,
9282                            "c:holeSize",
9283                            &size.to_string(),
9284                            markup.hole_size.as_ref(),
9285                        )?;
9286                    }
9287                    emit_raw(writer, markup.raw_children.at(trailing + 3))?;
9288                }
9289            }
9290            writer
9291                .write_event(Event::End(BytesEnd::new(&tag)))
9292                .map_err(OxmlError::from)?;
9293        }
9294    }
9295    Ok(())
9296}
9297
9298fn series_original_to_current(original: &[(u32, u32)], current: &[Series]) -> Vec<Option<usize>> {
9299    let mut used = vec![false; current.len()];
9300    let mut matches = vec![None; original.len()];
9301    for original_index in 0..original.len().min(current.len()) {
9302        if original[original_index]
9303            == (current[original_index].index, current[original_index].order)
9304        {
9305            matches[original_index] = Some(original_index);
9306            used[original_index] = true;
9307        }
9308    }
9309    for (original_index, key) in original.iter().enumerate() {
9310        if matches[original_index].is_some() {
9311            continue;
9312        }
9313        if let Some(current_index) = current
9314            .iter()
9315            .enumerate()
9316            .position(|(index, item)| !used[index] && (item.index, item.order) == *key)
9317        {
9318            matches[original_index] = Some(current_index);
9319            used[current_index] = true;
9320        }
9321    }
9322    match_unidentified_by_position(&mut matches, &mut used);
9323    matches
9324}
9325
9326fn axis_original_to_current(original: &[AxisId], current: &[AxisId]) -> Vec<Option<usize>> {
9327    let mut used = vec![false; current.len()];
9328    let mut matches = vec![None; original.len()];
9329    for (original_index, id) in original.iter().enumerate() {
9330        if let Some(current_index) = current
9331            .iter()
9332            .enumerate()
9333            .position(|(index, current_id)| !used[index] && current_id == id)
9334        {
9335            matches[original_index] = Some(current_index);
9336            used[current_index] = true;
9337        }
9338    }
9339    match_unidentified_by_position(&mut matches, &mut used);
9340    matches
9341}
9342
9343fn match_unidentified_by_position(matches: &mut [Option<usize>], used: &mut [bool]) {
9344    for original_index in 0..matches.len() {
9345        if matches[original_index].is_none() && original_index < used.len() && !used[original_index]
9346        {
9347            matches[original_index] = Some(original_index);
9348            used[original_index] = true;
9349        }
9350    }
9351    for matched in matches.iter_mut().filter(|matched| matched.is_none()) {
9352        if let Some(current_index) = used.iter().position(|used| !used) {
9353            *matched = Some(current_index);
9354            used[current_index] = true;
9355        }
9356    }
9357}
9358
9359fn invert_original_to_current(
9360    original_to_current: &[Option<usize>],
9361    current_len: usize,
9362) -> Vec<Option<usize>> {
9363    let mut current_to_original = vec![None; current_len];
9364    for (original, current) in original_to_current.iter().enumerate() {
9365        if let Some(current) = current {
9366            current_to_original[*current] = Some(original);
9367        }
9368    }
9369    current_to_original
9370}
9371
9372fn emit_repeated_raw<W: Write>(
9373    writer: &mut Writer<W>,
9374    raw_children: &OrderedRawChildren,
9375    offset: usize,
9376    original_to_current: &[Option<usize>],
9377    current_len: usize,
9378    current_boundary: usize,
9379) -> Result<()> {
9380    if current_boundary == 0 {
9381        emit_raw(writer, raw_children.at(offset))?;
9382    }
9383    for original_boundary in 1..=original_to_current.len() {
9384        let effective = original_to_current
9385            .iter()
9386            .skip(original_boundary)
9387            .flatten()
9388            .copied()
9389            .next()
9390            .unwrap_or(current_len);
9391        if effective == current_boundary {
9392            emit_raw(writer, raw_children.at(offset + original_boundary))?;
9393        }
9394    }
9395    Ok(())
9396}
9397
9398fn parse_plot_axis(xml: &[u8], inherited: &NamespaceBindings) -> Result<Option<Axis>> {
9399    let mut reader = Reader::from_reader(xml);
9400    let mut buffer = Vec::new();
9401    loop {
9402        match reader
9403            .read_event_into(&mut buffer)
9404            .map_err(OxmlError::from)?
9405        {
9406            Event::Start(element) | Event::Empty(element) => {
9407                if !element_is_in_namespace(&element, C_NS, inherited)? {
9408                    return Ok(None);
9409                }
9410                let name = element.name();
9411                if AxisKind::parse(local_name(name.as_ref())).is_none() {
9412                    return Ok(None);
9413                }
9414                return Axis::from_xml_with_namespaces(xml, inherited).map(Some);
9415            }
9416            Event::Eof => return Ok(None),
9417            _ => {}
9418        }
9419        buffer.clear();
9420    }
9421}
9422
9423fn validate_axis_pairs(axes: &[Axis]) -> Result<()> {
9424    for (index, axis) in axes.iter().enumerate() {
9425        if axes[..index].iter().any(|other| other.id == axis.id) {
9426            return Err(ChartError::DuplicateElement(format!(
9427                "c:axId {}",
9428                axis.id.value()
9429            )));
9430        }
9431    }
9432    for axis in axes {
9433        if axis.id == axis.cross_axis {
9434            return Err(ChartError::InvalidValue {
9435                element: "c:crossAx".to_owned(),
9436                value: format!("axis {} crosses itself", axis.id.value()),
9437            });
9438        }
9439        let crossed = axes
9440            .iter()
9441            .find(|candidate| candidate.id == axis.cross_axis)
9442            .ok_or_else(|| ChartError::InvalidValue {
9443                element: "c:crossAx".to_owned(),
9444                value: format!(
9445                    "axis {} references missing axis {}",
9446                    axis.id.value(),
9447                    axis.cross_axis.value()
9448                ),
9449            })?;
9450        if crossed.cross_axis != axis.id {
9451            return Err(ChartError::InvalidValue {
9452                element: "c:crossAx".to_owned(),
9453                value: format!(
9454                    "axis {} references {}, which references {}",
9455                    axis.id.value(),
9456                    crossed.id.value(),
9457                    crossed.cross_axis.value()
9458                ),
9459            });
9460        }
9461    }
9462    Ok(())
9463}
9464
9465fn parse_plot_series(
9466    xml: &[u8],
9467    inherited: &NamespaceBindings,
9468    series: &mut Vec<Series>,
9469) -> Result<()> {
9470    let mut reader = Reader::from_reader(xml);
9471    let mut buffer = Vec::new();
9472    loop {
9473        match reader
9474            .read_event_into(&mut buffer)
9475            .map_err(OxmlError::from)?
9476        {
9477            Event::Start(element) => {
9478                if !element_is_in_namespace(&element, C_NS, inherited)? {
9479                    return Ok(());
9480                }
9481                let local = local_name(element.name().as_ref()).to_vec();
9482                if !is_supported_series_plot(&local) {
9483                    return Ok(());
9484                }
9485                let namespaces = chart_bindings(inherited, &element)?;
9486                let mut inner = Vec::new();
9487                loop {
9488                    match reader
9489                        .read_event_into(&mut inner)
9490                        .map_err(OxmlError::from)?
9491                    {
9492                        Event::Start(child) => {
9493                            let name = chart_child_local(&child, &namespaces)?;
9494                            let raw = capture_element(&mut reader, &child)?;
9495                            if name.as_deref() == Some(b"ser") {
9496                                series.push(Series::from_xml_with_namespaces(&raw, &namespaces)?);
9497                            }
9498                        }
9499                        Event::Empty(child) => {
9500                            let name = chart_child_local(&child, &namespaces)?;
9501                            if name.as_deref() == Some(b"ser") {
9502                                return Err(ChartError::MissingElement("c:ser/c:idx".to_owned()));
9503                            }
9504                        }
9505                        Event::End(end) if matches_local_name(end.name().as_ref(), &local) => {
9506                            return Ok(());
9507                        }
9508                        Event::Eof => {
9509                            return Err(missing_end(&format!(
9510                                "c:{}",
9511                                String::from_utf8_lossy(&local)
9512                            )));
9513                        }
9514                        _ => {}
9515                    }
9516                    inner.clear();
9517                }
9518            }
9519            Event::Empty(_) | Event::Eof => return Ok(()),
9520            _ => {}
9521        }
9522        buffer.clear();
9523    }
9524}
9525
9526fn is_supported_series_plot(local: &[u8]) -> bool {
9527    matches!(
9528        local,
9529        b"areaChart"
9530            | b"area3DChart"
9531            | b"barChart"
9532            | b"bar3DChart"
9533            | b"doughnutChart"
9534            | b"lineChart"
9535            | b"line3DChart"
9536            | b"ofPieChart"
9537            | b"pieChart"
9538            | b"pie3DChart"
9539            | b"radarChart"
9540            | b"stockChart"
9541            | b"surfaceChart"
9542            | b"surface3DChart"
9543    )
9544}
9545
9546impl CT_Legend {
9547    fn from_xml(xml: &[u8]) -> Result<Self> {
9548        let (raw_attributes, raw_children) = parse_raw_shell(xml, b"legend", "c:legend")?;
9549        Ok(Self {
9550            raw_attributes,
9551            raw_children,
9552        })
9553    }
9554
9555    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9556        write_raw_shell(writer, "c:legend", &self.raw_attributes, &self.raw_children)
9557    }
9558
9559    pub fn raw_children(&self) -> &OrderedRawChildren {
9560        &self.raw_children
9561    }
9562}
9563
9564fn parse_raw_shell(
9565    xml: &[u8],
9566    local: &[u8],
9567    tag: &str,
9568) -> Result<(XmlAttributes, OrderedRawChildren)> {
9569    let mut reader = Reader::from_reader(xml);
9570    let mut buffer = Vec::new();
9571    loop {
9572        match reader
9573            .read_event_into(&mut buffer)
9574            .map_err(OxmlError::from)?
9575        {
9576            Event::Start(element) if matches_local_name(element.name().as_ref(), local) => {
9577                reject_conflicting_prefix(&element, b"c", C_NS)?;
9578                let raw_attributes = capture_attributes(&element)?;
9579                let mut raw_children = OrderedRawChildren::default();
9580                let mut child_buffer = Vec::new();
9581                loop {
9582                    match reader
9583                        .read_event_into(&mut child_buffer)
9584                        .map_err(OxmlError::from)?
9585                    {
9586                        Event::Start(child) => {
9587                            raw_children.push(0, capture_element(&mut reader, &child)?)
9588                        }
9589                        Event::Empty(child) => raw_children.push(0, capture_empty_element(&child)?),
9590                        event @ (Event::Text(_)
9591                        | Event::CData(_)
9592                        | Event::Comment(_)
9593                        | Event::PI(_)
9594                        | Event::GeneralRef(_)) => {
9595                            raw_children.push(0, capture_event(event)?);
9596                        }
9597                        Event::End(end) if matches_local_name(end.name().as_ref(), local) => break,
9598                        Event::Eof => return Err(missing_end(tag)),
9599                        _ => {}
9600                    }
9601                    child_buffer.clear();
9602                }
9603                return Ok((raw_attributes, raw_children));
9604            }
9605            Event::Empty(element) if matches_local_name(element.name().as_ref(), local) => {
9606                reject_conflicting_prefix(&element, b"c", C_NS)?;
9607                return Ok((capture_attributes(&element)?, OrderedRawChildren::default()));
9608            }
9609            Event::Start(element) | Event::Empty(element) => {
9610                return Err(ChartError::UnexpectedElement(element_name(&element)));
9611            }
9612            Event::Eof => return Err(ChartError::MissingElement(tag.to_owned())),
9613            _ => {}
9614        }
9615        buffer.clear();
9616    }
9617}
9618
9619fn write_raw_shell<W: Write>(
9620    writer: &mut Writer<W>,
9621    tag: &str,
9622    raw_attributes: &XmlAttributes,
9623    raw_children: &OrderedRawChildren,
9624) -> Result<()> {
9625    let mut start = BytesStart::new(tag);
9626    push_attributes(&mut start, raw_attributes);
9627    if raw_children.is_empty() {
9628        writer
9629            .write_event(Event::Empty(start))
9630            .map_err(OxmlError::from)?;
9631        return Ok(());
9632    }
9633    writer
9634        .write_event(Event::Start(start))
9635        .map_err(OxmlError::from)?;
9636    emit_raw(writer, raw_children.at(0))?;
9637    writer
9638        .write_event(Event::End(BytesEnd::new(tag)))
9639        .map_err(OxmlError::from)?;
9640    Ok(())
9641}
9642
9643/// The implemented behavior-bearing core of one `c:chart`.
9644#[derive(Clone, Debug, PartialEq)]
9645pub struct CT_Chart {
9646    pub title: Option<CT_Title>,
9647    pub auto_title_deleted: bool,
9648    pub plot_area: CT_PlotArea,
9649    pub legend: Option<CT_Legend>,
9650    pub plot_vis_only: bool,
9651    pub disp_blanks_as: DispBlanksAs,
9652    auto_title_deleted_markup: Option<ScalarMarkup>,
9653    plot_vis_only_markup: Option<ScalarMarkup>,
9654    disp_blanks_as_markup: Option<ScalarMarkup>,
9655    raw_attributes: Vec<(String, String)>,
9656    raw_children: OrderedRawChildren,
9657}
9658
9659impl CT_Chart {
9660    fn from_xml_with_namespaces(xml: &[u8], inherited: &NamespaceBindings) -> Result<Self> {
9661        let mut reader = Reader::from_reader(xml);
9662        let mut buffer = Vec::new();
9663        loop {
9664            match reader
9665                .read_event_into(&mut buffer)
9666                .map_err(OxmlError::from)?
9667            {
9668                Event::Start(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9669                    reject_conflicting_prefix(&element, b"c", C_NS)?;
9670                    return Self::from_element(&mut reader, &element, inherited);
9671                }
9672                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9673                    return Err(ChartError::MissingElement("c:plotArea".to_owned()));
9674                }
9675                Event::Start(element) | Event::Empty(element) => {
9676                    return Err(ChartError::UnexpectedElement(element_name(&element)));
9677                }
9678                Event::Eof => return Err(ChartError::MissingElement("c:chart".to_owned())),
9679                _ => {}
9680            }
9681            buffer.clear();
9682        }
9683    }
9684
9685    fn from_element(
9686        reader: &mut Reader<&[u8]>,
9687        start: &BytesStart<'_>,
9688        inherited: &NamespaceBindings,
9689    ) -> Result<Self> {
9690        let namespaces = bindings_with_local(inherited, start)?;
9691        let raw_attributes = capture_attributes(start)?;
9692        let mut state = ChartParseState::default();
9693        let mut buffer = Vec::new();
9694
9695        loop {
9696            match reader
9697                .read_event_into(&mut buffer)
9698                .map_err(OxmlError::from)?
9699            {
9700                Event::Start(element) => {
9701                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9702                        local_name(element.name().as_ref()).to_vec()
9703                    } else {
9704                        Vec::new()
9705                    };
9706                    let raw = capture_element(reader, &element)?;
9707                    state.parse_child(&name, raw, &namespaces)?;
9708                }
9709                Event::Empty(element) => {
9710                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9711                        local_name(element.name().as_ref()).to_vec()
9712                    } else {
9713                        Vec::new()
9714                    };
9715                    let raw = capture_empty_element(&element)?;
9716                    state.parse_child(&name, raw, &namespaces)?;
9717                }
9718                event @ (Event::Text(_)
9719                | Event::CData(_)
9720                | Event::Comment(_)
9721                | Event::PI(_)
9722                | Event::GeneralRef(_)) => {
9723                    state.capture_event(capture_event(event)?);
9724                }
9725                Event::End(element) if matches_local_name(element.name().as_ref(), b"chart") => {
9726                    break;
9727                }
9728                Event::Eof => return Err(missing_end("c:chart")),
9729                _ => {}
9730            }
9731            buffer.clear();
9732        }
9733
9734        state.finish(raw_attributes)
9735    }
9736
9737    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9738        let mut start = BytesStart::new("c:chart");
9739        push_attributes(&mut start, &self.raw_attributes);
9740        writer
9741            .write_event(Event::Start(start))
9742            .map_err(OxmlError::from)?;
9743        emit_raw(writer, self.raw_children.at(0))?;
9744        if let Some(title) = &self.title {
9745            title.write_xml(writer)?;
9746        }
9747        emit_raw(writer, self.raw_children.at(1))?;
9748        if self.auto_title_deleted_markup.is_some() || self.auto_title_deleted {
9749            write_scalar(
9750                writer,
9751                "c:autoTitleDeleted",
9752                if self.auto_title_deleted { "1" } else { "0" },
9753                self.auto_title_deleted_markup.as_ref(),
9754            )?;
9755        }
9756        for raw_boundary in 2..8 {
9757            emit_raw(writer, self.raw_children.at(raw_boundary))?;
9758        }
9759        self.plot_area.write_xml(writer)?;
9760        emit_raw(writer, self.raw_children.at(8))?;
9761        if let Some(legend) = &self.legend {
9762            legend.write_xml(writer)?;
9763        }
9764        emit_raw(writer, self.raw_children.at(9))?;
9765        if self.plot_vis_only_markup.is_some() || !self.plot_vis_only {
9766            write_scalar(
9767                writer,
9768                "c:plotVisOnly",
9769                if self.plot_vis_only { "1" } else { "0" },
9770                self.plot_vis_only_markup.as_ref(),
9771            )?;
9772        }
9773        emit_raw(writer, self.raw_children.at(10))?;
9774        if self.disp_blanks_as_markup.is_some() || self.disp_blanks_as != DispBlanksAs::Gap {
9775            write_scalar(
9776                writer,
9777                "c:dispBlanksAs",
9778                self.disp_blanks_as.as_str(),
9779                self.disp_blanks_as_markup.as_ref(),
9780            )?;
9781        }
9782        for raw_boundary in 11..=13 {
9783            emit_raw(writer, self.raw_children.at(raw_boundary))?;
9784        }
9785        writer
9786            .write_event(Event::End(BytesEnd::new("c:chart")))
9787            .map_err(OxmlError::from)?;
9788        Ok(())
9789    }
9790
9791    pub fn raw_children(&self) -> &OrderedRawChildren {
9792        &self.raw_children
9793    }
9794}
9795
9796#[derive(Default)]
9797struct ChartParseState {
9798    title: Option<CT_Title>,
9799    auto_title_deleted: Option<(bool, ScalarMarkup)>,
9800    plot_area: Option<CT_PlotArea>,
9801    legend: Option<CT_Legend>,
9802    plot_vis_only: Option<(bool, ScalarMarkup)>,
9803    disp_blanks_as: Option<(DispBlanksAs, ScalarMarkup)>,
9804    raw_children: OrderedRawChildren,
9805    boundary: usize,
9806}
9807
9808impl ChartParseState {
9809    fn capture_event(&mut self, raw: Vec<u8>) {
9810        self.raw_children.push(self.boundary, raw);
9811    }
9812
9813    fn parse_child(
9814        &mut self,
9815        name: &[u8],
9816        raw: Vec<u8>,
9817        namespaces: &NamespaceBindings,
9818    ) -> Result<()> {
9819        match name {
9820            b"title" => {
9821                set_once(&mut self.title, CT_Title::from_xml(&raw)?, "c:title")?;
9822                self.boundary = self.boundary.max(1);
9823            }
9824            b"autoTitleDeleted" => {
9825                set_once(
9826                    &mut self.auto_title_deleted,
9827                    parse_bool_value(&raw, "autoTitleDeleted")?,
9828                    "c:autoTitleDeleted",
9829                )?;
9830                self.boundary = self.boundary.max(2);
9831            }
9832            b"plotArea" => {
9833                set_once(
9834                    &mut self.plot_area,
9835                    CT_PlotArea::from_xml_with_namespaces(&raw, namespaces)?,
9836                    "c:plotArea",
9837                )?;
9838                self.boundary = self.boundary.max(8);
9839            }
9840            b"legend" => {
9841                set_once(&mut self.legend, CT_Legend::from_xml(&raw)?, "c:legend")?;
9842                self.boundary = self.boundary.max(9);
9843            }
9844            b"plotVisOnly" => {
9845                set_once(
9846                    &mut self.plot_vis_only,
9847                    parse_bool_value(&raw, "plotVisOnly")?,
9848                    "c:plotVisOnly",
9849                )?;
9850                self.boundary = self.boundary.max(10);
9851            }
9852            b"dispBlanksAs" => {
9853                set_once(
9854                    &mut self.disp_blanks_as,
9855                    parse_enum_value(&raw, "dispBlanksAs")?,
9856                    "c:dispBlanksAs",
9857                )?;
9858                self.boundary = self.boundary.max(11);
9859            }
9860            _ => {
9861                self.raw_children
9862                    .push(chart_raw_slot(name).unwrap_or(self.boundary), raw);
9863                self.boundary = self.boundary.max(chart_raw_boundary_after(name));
9864            }
9865        }
9866        Ok(())
9867    }
9868
9869    fn finish(self, raw_attributes: XmlAttributes) -> Result<CT_Chart> {
9870        let (auto_title_deleted, auto_title_deleted_markup) = self
9871            .auto_title_deleted
9872            .map(|(value, markup)| (value, Some(markup)))
9873            .unwrap_or((false, None));
9874        let (plot_vis_only, plot_vis_only_markup) = self
9875            .plot_vis_only
9876            .map(|(value, markup)| (value, Some(markup)))
9877            .unwrap_or((true, None));
9878        let (disp_blanks_as, disp_blanks_as_markup) = self
9879            .disp_blanks_as
9880            .map(|(value, markup)| (value, Some(markup)))
9881            .unwrap_or((DispBlanksAs::Gap, None));
9882        Ok(CT_Chart {
9883            title: self.title,
9884            auto_title_deleted,
9885            plot_area: self
9886                .plot_area
9887                .ok_or_else(|| ChartError::MissingElement("c:plotArea".to_owned()))?,
9888            legend: self.legend,
9889            plot_vis_only,
9890            disp_blanks_as,
9891            auto_title_deleted_markup,
9892            plot_vis_only_markup,
9893            disp_blanks_as_markup,
9894            raw_attributes,
9895            raw_children: raw_children_in_schema_order(&self.raw_children, 13),
9896        })
9897    }
9898}
9899
9900fn chart_raw_slot(name: &[u8]) -> Option<usize> {
9901    match name {
9902        b"pivotFmts" => Some(2),
9903        b"view3D" => Some(3),
9904        b"floor" => Some(4),
9905        b"sideWall" => Some(5),
9906        b"backWall" => Some(6),
9907        // Both unmodelled tail children share a slot so producer sibling order
9908        // remains byte-stable until a later story types either element.
9909        b"showDLblsOverMax" | b"extLst" => Some(11),
9910        _ => None,
9911    }
9912}
9913
9914fn chart_raw_boundary_after(name: &[u8]) -> usize {
9915    match name {
9916        b"title" => 1,
9917        b"autoTitleDeleted" => 2,
9918        b"pivotFmts" => 3,
9919        b"view3D" => 4,
9920        b"floor" => 5,
9921        b"sideWall" => 6,
9922        b"backWall" => 7,
9923        b"plotArea" => 8,
9924        b"legend" => 9,
9925        b"plotVisOnly" => 10,
9926        b"dispBlanksAs" => 11,
9927        b"showDLblsOverMax" | b"extLst" => 11,
9928        _ => 0,
9929    }
9930}
9931
9932/// One complete `c:chartSpace` part with the F-118 core typed.
9933#[derive(Clone, Debug, PartialEq)]
9934pub struct CT_ChartSpace {
9935    pub chart: CT_Chart,
9936    pub sp_pr: Option<CT_ShapeProperties>,
9937    pub tx_pr: Option<CT_TextBody>,
9938    raw_attributes: Vec<(String, String)>,
9939    namespace_declarations: Vec<(String, String)>,
9940    raw_children: OrderedRawChildren,
9941}
9942
9943impl CT_ChartSpace {
9944    /// Parses one complete ChartML chart-space part.
9945    pub fn from_xml(xml: &[u8]) -> Result<Self> {
9946        let mut reader = Reader::from_reader(xml);
9947        let mut buffer = Vec::new();
9948        loop {
9949            match reader
9950                .read_event_into(&mut buffer)
9951                .map_err(OxmlError::from)?
9952            {
9953                Event::Start(element)
9954                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
9955                {
9956                    validate_chart_space_namespace(&element)?;
9957                    return Self::from_element(&mut reader, &element);
9958                }
9959                Event::Empty(element)
9960                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
9961                {
9962                    validate_chart_space_namespace(&element)?;
9963                    return Err(ChartError::MissingElement("c:chart".to_owned()));
9964                }
9965                Event::Start(element) | Event::Empty(element) => {
9966                    return Err(ChartError::UnexpectedElement(element_name(&element)));
9967                }
9968                Event::Eof => return Err(ChartError::MissingElement("c:chartSpace".to_owned())),
9969                _ => {}
9970            }
9971            buffer.clear();
9972        }
9973    }
9974
9975    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
9976        let namespaces = namespace_bindings(start)?;
9977        let (raw_attributes, namespace_declarations) = capture_root_attributes(start)?;
9978        let mut state = ChartSpaceParseState::default();
9979        let mut buffer = Vec::new();
9980
9981        loop {
9982            match reader
9983                .read_event_into(&mut buffer)
9984                .map_err(OxmlError::from)?
9985            {
9986                Event::Start(element) => {
9987                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9988                        local_name(element.name().as_ref()).to_vec()
9989                    } else {
9990                        Vec::new()
9991                    };
9992                    let raw = capture_element(reader, &element)?;
9993                    state.parse_child(&name, raw, &namespaces)?;
9994                }
9995                Event::Empty(element) => {
9996                    let name = if element_is_in_namespace(&element, C_NS, &namespaces)? {
9997                        local_name(element.name().as_ref()).to_vec()
9998                    } else {
9999                        Vec::new()
10000                    };
10001                    let raw = capture_empty_element(&element)?;
10002                    state.parse_child(&name, raw, &namespaces)?;
10003                }
10004                event @ (Event::Text(_)
10005                | Event::CData(_)
10006                | Event::Comment(_)
10007                | Event::PI(_)
10008                | Event::GeneralRef(_)) => {
10009                    state.capture_event(capture_event(event)?);
10010                }
10011                Event::End(element)
10012                    if matches_local_name(element.name().as_ref(), b"chartSpace") =>
10013                {
10014                    break;
10015                }
10016                Event::Eof => return Err(missing_end("c:chartSpace")),
10017                _ => {}
10018            }
10019            buffer.clear();
10020        }
10021
10022        state.finish(raw_attributes, namespace_declarations)
10023    }
10024
10025    /// Writes one chart-space part using fixed OOXML prefixes and schema order.
10026    pub fn to_xml(&self) -> Result<Vec<u8>> {
10027        let mut writer = Writer::new(Vec::new());
10028        let mut start = BytesStart::new("c:chartSpace");
10029        start.push_attribute(("xmlns:c", C_NS));
10030        start.push_attribute(("xmlns:a", A_NS));
10031        start.push_attribute(("xmlns:r", R_NS));
10032        push_attributes(&mut start, &self.namespace_declarations);
10033        push_attributes(&mut start, &self.raw_attributes);
10034        writer
10035            .write_event(Event::Start(start))
10036            .map_err(OxmlError::from)?;
10037        for raw_boundary in 0..8 {
10038            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10039        }
10040        self.chart.write_xml(&mut writer)?;
10041        emit_raw(&mut writer, self.raw_children.at(8))?;
10042        if let Some(properties) = &self.sp_pr {
10043            properties.write_xml_as(&mut writer, "c:spPr")?;
10044        }
10045        emit_raw(&mut writer, self.raw_children.at(9))?;
10046        if let Some(text) = &self.tx_pr {
10047            text.write_xml_as(&mut writer, "c:txPr")?;
10048        }
10049        for raw_boundary in 10..=14 {
10050            emit_raw(&mut writer, self.raw_children.at(raw_boundary))?;
10051        }
10052        writer
10053            .write_event(Event::End(BytesEnd::new("c:chartSpace")))
10054            .map_err(OxmlError::from)?;
10055        Ok(writer.into_inner())
10056    }
10057
10058    pub fn raw_children(&self) -> &OrderedRawChildren {
10059        &self.raw_children
10060    }
10061}
10062
10063#[derive(Default)]
10064struct ChartSpaceParseState {
10065    chart: Option<CT_Chart>,
10066    sp_pr: Option<CT_ShapeProperties>,
10067    tx_pr: Option<CT_TextBody>,
10068    raw_children: OrderedRawChildren,
10069    boundary: usize,
10070}
10071
10072impl ChartSpaceParseState {
10073    fn capture_event(&mut self, raw: Vec<u8>) {
10074        self.raw_children.push(self.boundary, raw);
10075    }
10076
10077    fn parse_child(
10078        &mut self,
10079        name: &[u8],
10080        raw: Vec<u8>,
10081        namespaces: &NamespaceBindings,
10082    ) -> Result<()> {
10083        match name {
10084            b"chart" => {
10085                set_once(
10086                    &mut self.chart,
10087                    CT_Chart::from_xml_with_namespaces(&raw, namespaces)?,
10088                    "c:chart",
10089                )?;
10090                self.boundary = self.boundary.max(8);
10091            }
10092            b"spPr" => {
10093                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10094                let properties = CT_ShapeProperties::from_xml(&raw)?;
10095                let mut writer = Writer::new(Vec::new());
10096                properties.write_xml_as(&mut writer, "c:spPr")?;
10097                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"spPr")?;
10098                set_once(&mut self.sp_pr, properties, "c:spPr")?;
10099                self.boundary = self.boundary.max(9);
10100            }
10101            b"txPr" => {
10102                reject_conflicting_prefix_in_xml(&raw, b"a", A_NS)?;
10103                let text = CT_TextBody::from_xml_as(&raw, b"txPr")?;
10104                let mut writer = Writer::new(Vec::new());
10105                text.write_xml_as(&mut writer, "c:txPr")?;
10106                reject_rewritten_foreign_elements(&raw, &writer.into_inner(), namespaces, b"txPr")?;
10107                set_once(&mut self.tx_pr, text, "c:txPr")?;
10108                self.boundary = self.boundary.max(10);
10109            }
10110            _ => {
10111                self.raw_children
10112                    .push(chart_space_raw_slot(name).unwrap_or(self.boundary), raw);
10113                self.boundary = self.boundary.max(chart_space_raw_boundary_after(name));
10114            }
10115        }
10116        Ok(())
10117    }
10118
10119    fn finish(
10120        self,
10121        raw_attributes: XmlAttributes,
10122        namespace_declarations: XmlAttributes,
10123    ) -> Result<CT_ChartSpace> {
10124        Ok(CT_ChartSpace {
10125            chart: self
10126                .chart
10127                .ok_or_else(|| ChartError::MissingElement("c:chart".to_owned()))?,
10128            sp_pr: self.sp_pr,
10129            tx_pr: self.tx_pr,
10130            raw_attributes,
10131            namespace_declarations,
10132            raw_children: raw_children_in_schema_order(&self.raw_children, 14),
10133        })
10134    }
10135}
10136
10137fn raw_children_in_schema_order(
10138    raw_children: &OrderedRawChildren,
10139    final_boundary: usize,
10140) -> OrderedRawChildren {
10141    let mut ordered = OrderedRawChildren::default();
10142    for boundary in 0..=final_boundary {
10143        for raw in raw_children.at(boundary) {
10144            ordered.push(boundary, raw.to_vec());
10145        }
10146    }
10147    ordered
10148}
10149
10150fn chart_space_raw_slot(name: &[u8]) -> Option<usize> {
10151    match name {
10152        b"date1904" => Some(0),
10153        b"lang" => Some(1),
10154        b"roundedCorners" => Some(2),
10155        b"style" => Some(3),
10156        b"clrMapOvr" => Some(4),
10157        b"pivotSource" => Some(5),
10158        b"protection" => Some(6),
10159        b"externalData" => Some(10),
10160        b"printSettings" => Some(11),
10161        b"userShapes" => Some(12),
10162        b"extLst" => Some(13),
10163        _ => None,
10164    }
10165}
10166
10167fn chart_space_raw_boundary_after(name: &[u8]) -> usize {
10168    match name {
10169        b"date1904" => 1,
10170        b"lang" => 2,
10171        b"roundedCorners" => 3,
10172        b"style" => 4,
10173        b"clrMapOvr" => 5,
10174        b"pivotSource" => 6,
10175        b"protection" => 7,
10176        b"chart" => 8,
10177        b"spPr" => 9,
10178        b"txPr" => 10,
10179        b"externalData" => 11,
10180        b"printSettings" => 12,
10181        b"userShapes" => 13,
10182        b"extLst" => 14,
10183        _ => 0,
10184    }
10185}
10186
10187fn parse_bool_value(xml: &[u8], local: &str) -> Result<(bool, ScalarMarkup)> {
10188    let (value, markup) = scalar_value(xml, local)?;
10189    let value = match value.as_deref().unwrap_or("1") {
10190        "1" | "true" => true,
10191        "0" | "false" => false,
10192        invalid => return Err(invalid_attribute(local, "val", invalid.to_owned())),
10193    };
10194    Ok((value, markup))
10195}
10196
10197fn parse_enum_value(xml: &[u8], local: &str) -> Result<(DispBlanksAs, ScalarMarkup)> {
10198    let (value, markup) = scalar_value(xml, local)?;
10199    let value = value.ok_or_else(|| invalid_attribute(local, "val", "<missing>".to_owned()))?;
10200    let parsed =
10201        DispBlanksAs::parse(&value).ok_or_else(|| invalid_attribute(local, "val", value))?;
10202    Ok((parsed, markup))
10203}
10204
10205fn scalar_value(xml: &[u8], expected: &str) -> Result<(Option<String>, ScalarMarkup)> {
10206    let mut reader = Reader::from_reader(xml);
10207    let mut buffer = Vec::new();
10208    loop {
10209        match reader
10210            .read_event_into(&mut buffer)
10211            .map_err(OxmlError::from)?
10212        {
10213            Event::Empty(element)
10214                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10215            {
10216                reject_conflicting_prefix(&element, b"c", C_NS)?;
10217                return Ok((
10218                    attribute_value(&element, b"val")?,
10219                    ScalarMarkup {
10220                        raw_attributes: capture_attributes_except(&element, b"val")?,
10221                        raw_content: Vec::new(),
10222                    },
10223                ));
10224            }
10225            Event::Start(element)
10226                if matches_local_name(element.name().as_ref(), expected.as_bytes()) =>
10227            {
10228                reject_conflicting_prefix(&element, b"c", C_NS)?;
10229                let value = attribute_value(&element, b"val")?;
10230                let raw_attributes = capture_attributes_except(&element, b"val")?;
10231                let mut raw_content = Vec::new();
10232                let mut inner = Vec::new();
10233                loop {
10234                    match reader
10235                        .read_event_into(&mut inner)
10236                        .map_err(OxmlError::from)?
10237                    {
10238                        Event::End(end)
10239                            if matches_local_name(end.name().as_ref(), expected.as_bytes()) =>
10240                        {
10241                            return Ok((
10242                                value,
10243                                ScalarMarkup {
10244                                    raw_attributes,
10245                                    raw_content,
10246                                },
10247                            ));
10248                        }
10249                        event @ (Event::Text(_)
10250                        | Event::CData(_)
10251                        | Event::Comment(_)
10252                        | Event::PI(_)
10253                        | Event::GeneralRef(_)) => raw_content.push(capture_event(event)?),
10254                        Event::Eof => return Err(missing_end(expected)),
10255                        _ => return Err(ChartError::UnexpectedElement(expected.to_owned())),
10256                    }
10257                    inner.clear();
10258                }
10259            }
10260            Event::Start(element) | Event::Empty(element) => {
10261                return Err(ChartError::UnexpectedElement(element_name(&element)));
10262            }
10263            Event::Eof => return Err(ChartError::MissingElement(expected.to_owned())),
10264            _ => {}
10265        }
10266        buffer.clear();
10267    }
10268}
10269
10270fn set_once<T>(slot: &mut Option<T>, value: T, name: &str) -> Result<()> {
10271    if slot.is_some() {
10272        return Err(ChartError::DuplicateElement(name.to_owned()));
10273    }
10274    *slot = Some(value);
10275    Ok(())
10276}
10277
10278fn mark_once(seen: &mut bool, name: &str) -> Result<()> {
10279    if *seen {
10280        return Err(ChartError::DuplicateElement(name.to_owned()));
10281    }
10282    *seen = true;
10283    Ok(())
10284}
10285
10286fn validate_chart_space_namespace(element: &BytesStart<'_>) -> Result<()> {
10287    let name = element.name();
10288    let qualified = name.as_ref();
10289    let declaration = match qualified.iter().position(|byte| *byte == b':') {
10290        Some(position) => {
10291            let mut declaration = b"xmlns:".to_vec();
10292            declaration.extend_from_slice(&qualified[..position]);
10293            declaration
10294        }
10295        None => b"xmlns".to_vec(),
10296    };
10297    let namespace = attribute_value(element, &declaration)?
10298        .ok_or_else(|| invalid_attribute("chartSpace", "namespace", "<missing>".to_owned()))?;
10299    if namespace != C_NS {
10300        return Err(invalid_attribute("chartSpace", "namespace", namespace));
10301    }
10302    reject_conflicting_prefix(element, b"c", C_NS)?;
10303    reject_conflicting_prefix(element, b"a", A_NS)?;
10304    reject_conflicting_prefix(element, b"r", R_NS)
10305}
10306
10307fn reject_conflicting_prefix_in_xml(xml: &[u8], prefix: &[u8], namespace: &str) -> Result<()> {
10308    let mut reader = Reader::from_reader(xml);
10309    let mut buffer = Vec::new();
10310    loop {
10311        match reader
10312            .read_event_into(&mut buffer)
10313            .map_err(OxmlError::from)?
10314        {
10315            Event::Start(element) | Event::Empty(element) => {
10316                return reject_conflicting_prefix(&element, prefix, namespace);
10317            }
10318            Event::Eof => return Err(ChartError::MissingElement("XML root".to_owned())),
10319            _ => {}
10320        }
10321        buffer.clear();
10322    }
10323}
10324
10325fn reject_rewritten_foreign_elements(
10326    original: &[u8],
10327    written: &[u8],
10328    inherited: &NamespaceBindings,
10329    root_local: &[u8],
10330) -> Result<()> {
10331    let original = foreign_element_regions(original, inherited, root_local)?;
10332    let mut written_namespaces = inherited.clone();
10333    written_namespaces.retain(|(prefix, _)| prefix != b"a");
10334    written_namespaces.push((b"a".to_vec(), A_NS.to_owned()));
10335    let written = foreign_element_regions(written, &written_namespaces, root_local)?;
10336    if original != written {
10337        return Err(invalid_attribute(
10338            &String::from_utf8_lossy(root_local),
10339            "namespace",
10340            "foreign DrawingML content would be converted to typed content".to_owned(),
10341        ));
10342    }
10343    Ok(())
10344}
10345
10346fn foreign_element_regions(
10347    xml: &[u8],
10348    inherited: &NamespaceBindings,
10349    root_local: &[u8],
10350) -> Result<Vec<Vec<u8>>> {
10351    let mut reader = Reader::from_reader(xml);
10352    let mut buffer = Vec::new();
10353    let root_namespaces = loop {
10354        match reader
10355            .read_event_into(&mut buffer)
10356            .map_err(OxmlError::from)?
10357        {
10358            Event::Start(element) if matches_local_name(element.name().as_ref(), root_local) => {
10359                break bindings_with_local(inherited, &element)?;
10360            }
10361            Event::Empty(element) if matches_local_name(element.name().as_ref(), root_local) => {
10362                return Ok(Vec::new());
10363            }
10364            Event::Start(element) | Event::Empty(element) => {
10365                return Err(ChartError::UnexpectedElement(element_name(&element)));
10366            }
10367            Event::Eof => {
10368                return Err(ChartError::MissingElement(
10369                    String::from_utf8_lossy(root_local).into_owned(),
10370                ));
10371            }
10372            _ => {}
10373        }
10374        buffer.clear();
10375    };
10376
10377    let mut namespace_stack = vec![root_namespaces];
10378    let mut foreign = Vec::new();
10379    let mut child_buffer = Vec::new();
10380    loop {
10381        match reader
10382            .read_event_into(&mut child_buffer)
10383            .map_err(OxmlError::from)?
10384        {
10385            Event::Start(element) => {
10386                let namespaces = namespace_stack.last().expect("root namespace binding");
10387                if element_is_in_namespace(&element, A_NS, namespaces)? {
10388                    namespace_stack.push(bindings_with_local(namespaces, &element)?);
10389                } else {
10390                    foreign.push(capture_element(&mut reader, &element)?);
10391                }
10392            }
10393            Event::Empty(element) => {
10394                let namespaces = namespace_stack.last().expect("root namespace binding");
10395                if !element_is_in_namespace(&element, A_NS, namespaces)? {
10396                    foreign.push(capture_empty_element(&element)?);
10397                }
10398            }
10399            Event::End(element) if matches_local_name(element.name().as_ref(), root_local) => {
10400                return Ok(foreign);
10401            }
10402            Event::End(_) => {
10403                namespace_stack.pop();
10404            }
10405            Event::Eof => {
10406                return Err(missing_end(&String::from_utf8_lossy(root_local)));
10407            }
10408            _ => {}
10409        }
10410        child_buffer.clear();
10411    }
10412}
10413
10414fn reject_conflicting_prefix(
10415    element: &BytesStart<'_>,
10416    prefix: &[u8],
10417    namespace: &str,
10418) -> Result<()> {
10419    let mut key = b"xmlns:".to_vec();
10420    key.extend_from_slice(prefix);
10421    if let Some(value) = attribute_value(element, &key)?
10422        && value != namespace
10423    {
10424        return Err(invalid_attribute(
10425            &element_name(element),
10426            &String::from_utf8_lossy(&key),
10427            value,
10428        ));
10429    }
10430    Ok(())
10431}
10432
10433fn namespace_bindings(element: &BytesStart<'_>) -> Result<NamespaceBindings> {
10434    let mut bindings = Vec::new();
10435    for attribute in element.attributes() {
10436        let attribute = attribute.map_err(OxmlError::from)?;
10437        let key = attribute.key.as_ref();
10438        let prefix = if key == b"xmlns" {
10439            Vec::new()
10440        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
10441            prefix.to_vec()
10442        } else {
10443            continue;
10444        };
10445        let namespace = attribute
10446            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10447            .map_err(OxmlError::from)?
10448            .into_owned();
10449        bindings.push((prefix, namespace));
10450    }
10451    Ok(bindings)
10452}
10453
10454fn bindings_with_local(
10455    inherited: &NamespaceBindings,
10456    element: &BytesStart<'_>,
10457) -> Result<NamespaceBindings> {
10458    let mut bindings = inherited.clone();
10459    for (prefix, namespace) in namespace_bindings(element)? {
10460        upsert_namespace_binding(&mut bindings, prefix, namespace);
10461    }
10462    Ok(bindings)
10463}
10464
10465fn upsert_namespace_binding(bindings: &mut NamespaceBindings, prefix: Vec<u8>, namespace: String) {
10466    if let Some((_, current)) = bindings.iter_mut().find(|(current, _)| current == &prefix) {
10467        *current = namespace;
10468    } else {
10469        bindings.push((prefix, namespace));
10470    }
10471}
10472
10473fn element_is_in_namespace(
10474    element: &BytesStart<'_>,
10475    expected: &str,
10476    inherited: &NamespaceBindings,
10477) -> Result<bool> {
10478    let name = element.name();
10479    let qualified = name.as_ref();
10480    let prefix = qualified
10481        .iter()
10482        .position(|byte| *byte == b':')
10483        .map(|position| &qualified[..position])
10484        .unwrap_or_default();
10485    let local = namespace_bindings(element)?;
10486    let namespace = local
10487        .iter()
10488        .rev()
10489        .chain(inherited.iter().rev())
10490        .find(|(candidate, _)| candidate.as_slice() == prefix)
10491        .map(|(_, namespace)| namespace.as_str());
10492    Ok(namespace == Some(expected))
10493}
10494
10495fn attribute_value(element: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
10496    for attribute in element.attributes() {
10497        let attribute = attribute.map_err(OxmlError::from)?;
10498        if attribute.key.as_ref() == name {
10499            return Ok(Some(
10500                attribute
10501                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
10502                    .map_err(OxmlError::from)?
10503                    .into_owned(),
10504            ));
10505        }
10506    }
10507    Ok(None)
10508}
10509
10510fn capture_root_attributes(start: &BytesStart<'_>) -> Result<RootAttributes> {
10511    let mut attributes = Vec::new();
10512    let mut namespaces = Vec::new();
10513    for attribute in start.attributes() {
10514        let attribute = attribute.map_err(OxmlError::from)?;
10515        let name = std::str::from_utf8(attribute.key.as_ref())
10516            .map_err(OxmlError::from)?
10517            .to_owned();
10518        let value = attribute
10519            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10520            .map_err(OxmlError::from)?
10521            .into_owned();
10522        if name == "xmlns:c" || name == "xmlns:a" || name == "xmlns:r" {
10523            continue;
10524        }
10525        if name == "xmlns" || name.starts_with("xmlns:") {
10526            namespaces.push((name, value));
10527        } else {
10528            attributes.push((name, value));
10529        }
10530    }
10531    Ok((attributes, namespaces))
10532}
10533
10534fn capture_attributes(start: &BytesStart<'_>) -> Result<XmlAttributes> {
10535    let mut attributes = Vec::new();
10536    for attribute in start.attributes() {
10537        let attribute = attribute.map_err(OxmlError::from)?;
10538        attributes.push((
10539            std::str::from_utf8(attribute.key.as_ref())
10540                .map_err(OxmlError::from)?
10541                .to_owned(),
10542            attribute
10543                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10544                .map_err(OxmlError::from)?
10545                .into_owned(),
10546        ));
10547    }
10548    Ok(attributes)
10549}
10550
10551fn capture_attributes_except(start: &BytesStart<'_>, excluded: &[u8]) -> Result<XmlAttributes> {
10552    let mut attributes = Vec::new();
10553    for attribute in start.attributes() {
10554        let attribute = attribute.map_err(OxmlError::from)?;
10555        if attribute.key.as_ref() == excluded {
10556            continue;
10557        }
10558        attributes.push((
10559            std::str::from_utf8(attribute.key.as_ref())
10560                .map_err(OxmlError::from)?
10561                .to_owned(),
10562            attribute
10563                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10564                .map_err(OxmlError::from)?
10565                .into_owned(),
10566        ));
10567    }
10568    Ok(attributes)
10569}
10570
10571fn capture_attributes_excluding(
10572    start: &BytesStart<'_>,
10573    excluded: &[&[u8]],
10574) -> Result<XmlAttributes> {
10575    let mut attributes = Vec::new();
10576    for attribute in start.attributes() {
10577        let attribute = attribute.map_err(OxmlError::from)?;
10578        if excluded.contains(&attribute.key.as_ref()) {
10579            continue;
10580        }
10581        attributes.push((
10582            std::str::from_utf8(attribute.key.as_ref())
10583                .map_err(OxmlError::from)?
10584                .to_owned(),
10585            attribute
10586                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
10587                .map_err(OxmlError::from)?
10588                .into_owned(),
10589        ));
10590    }
10591    Ok(attributes)
10592}
10593
10594fn push_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
10595    for (name, value) in attributes {
10596        start.push_attribute((name.as_str(), value.as_str()));
10597    }
10598}
10599
10600fn write_scalar<W: Write>(
10601    writer: &mut Writer<W>,
10602    name: &str,
10603    value: &str,
10604    markup: Option<&ScalarMarkup>,
10605) -> Result<()> {
10606    let mut start = BytesStart::new(name);
10607    start.push_attribute(("val", value));
10608    if let Some(markup) = markup {
10609        push_attributes(&mut start, &markup.raw_attributes);
10610    }
10611    let has_content = markup.is_some_and(|markup| !markup.raw_content.is_empty());
10612    if !has_content {
10613        writer
10614            .write_event(Event::Empty(start))
10615            .map_err(OxmlError::from)?;
10616        return Ok(());
10617    }
10618    writer
10619        .write_event(Event::Start(start))
10620        .map_err(OxmlError::from)?;
10621    if let Some(markup) = markup {
10622        for raw in &markup.raw_content {
10623            writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
10624        }
10625    }
10626    writer
10627        .write_event(Event::End(BytesEnd::new(name)))
10628        .map_err(OxmlError::from)?;
10629    Ok(())
10630}
10631
10632fn capture_event(event: Event<'_>) -> Result<Vec<u8>> {
10633    let mut writer = Writer::new(Vec::new());
10634    writer
10635        .write_event(event.into_owned())
10636        .map_err(OxmlError::from)?;
10637    Ok(writer.into_inner())
10638}
10639
10640fn emit_raw<'a, W: Write>(
10641    writer: &mut Writer<W>,
10642    children: impl Iterator<Item = &'a [u8]>,
10643) -> Result<()> {
10644    for child in children {
10645        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
10646    }
10647    Ok(())
10648}
10649
10650fn invalid_attribute(element: &str, attribute: &str, value: String) -> ChartError {
10651    ChartError::InvalidAttribute {
10652        element: element.to_owned(),
10653        attribute: attribute.to_owned(),
10654        value,
10655    }
10656}
10657
10658fn element_name(element: &BytesStart<'_>) -> String {
10659    String::from_utf8_lossy(element.name().as_ref()).into_owned()
10660}
10661
10662fn missing_end(element: &str) -> ChartError {
10663    ChartError::Xml(OxmlError::MissingElement(format!("closing {element}")))
10664}
10665
10666#[cfg(test)]
10667mod tests {
10668    use std::collections::HashSet;
10669    use std::fs;
10670    use std::path::{Path, PathBuf};
10671    use std::process::Command;
10672
10673    use oxml_core::raw_xml::{capture_element, capture_empty_element};
10674    use oxml_drawing::color::{ColorMap, ColorMapSlot, RgbColor, ThemeColorSlot};
10675    use oxml_drawing::theme::CT_OfficeStyleSheet;
10676    use oxml_layout::{
10677        Color, FontManager, LayoutResult, PageFrame, Paint, PathCommand, Point, PositionedElement,
10678        Rect,
10679    };
10680    use oxml_opc::OpcPackage;
10681    use quick_xml::Reader;
10682    use quick_xml::events::Event;
10683
10684    use super::{
10685        A_NS, Axis, AxisData, AxisId, AxisKind, AxisPosition, BarDirection, BarGrouping, C_NS,
10686        CT_ChartSpace, CT_DLbls, CT_ShapeProperties, CT_TextBody, ChartGeometry, DataLabelPosition,
10687        DispBlanksAs, Domain, Grouping, NumberFormat, NumericData, Orientation, Plot, R_NS,
10688        ScatterStyle, Series, StringRef, TickLabelPosition, TickMark, capture_event, local_name,
10689        matches_local_name, nice_number_scale, render_chart as render_chart_with_theme,
10690        render_geometry as render_geometry_with_theme,
10691    };
10692
10693    const MANIFEST: &str = include_str!("../../../scripts/pptx-corpus-manifest.tsv");
10694    const EXPECTED_DECKS: usize = 50;
10695    const LIBREOFFICE_VERSION: &str =
10696        "LibreOffice 26.2.5.2 cd7284b4cbbfeb507e630c1aac019f4157393acb";
10697    const PDFTOTEXT_VERSION: &str = "pdftotext version 26.01.0";
10698    const PDFTOPPM_VERSION: &str = "pdftoppm version 26.01.0";
10699    const PLOT_RENDER_NORMALIZED_MAE_THRESHOLD: f64 = 0.0;
10700
10701    fn render_geometry(chart: &super::CT_Chart, bounds: Rect) -> super::Result<ChartGeometry> {
10702        render_geometry_with_theme(
10703            chart,
10704            bounds,
10705            &CT_OfficeStyleSheet::office_default(),
10706            &ColorMap::default(),
10707        )
10708    }
10709
10710    fn render_chart(
10711        chart: &super::CT_Chart,
10712        bounds: Rect,
10713        fonts: &mut FontManager,
10714    ) -> super::Result<oxml_layout::GroupElement> {
10715        render_chart_with_theme(
10716            chart,
10717            bounds,
10718            &CT_OfficeStyleSheet::office_default(),
10719            &ColorMap::default(),
10720            fonts,
10721        )
10722    }
10723
10724    #[test]
10725    fn bar_chart_rasterises_at_computed_positions() {
10726        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
10727        let geometry = render_geometry(
10728            &chart.chart,
10729            Rect {
10730                x: 0.0,
10731                y: 0.0,
10732                width: 200.0,
10733                height: 140.0,
10734            },
10735        )
10736        .unwrap();
10737        assert_eq!(
10738            geometry.plot_bounds,
10739            Rect {
10740                x: 36.0,
10741                y: 12.0,
10742                width: 152.0,
10743                height: 100.0,
10744            }
10745        );
10746        let bounds = path_bounds(&geometry);
10747        assert_eq!(bounds.len(), 2);
10748        assert_rect_near(
10749            bounds[0],
10750            Rect {
10751                x: 58.8,
10752                y: 62.0,
10753                width: 30.4,
10754                height: 50.0,
10755            },
10756        );
10757        assert_rect_near(
10758            bounds[1],
10759            Rect {
10760                x: 134.8,
10761                y: 12.0,
10762                width: 30.4,
10763                height: 100.0,
10764            },
10765        );
10766
10767        let layout = LayoutResult::new(
10768            vec![PageFrame::new(1, 200.0, 140.0, geometry.elements)],
10769            Vec::new(),
10770            None,
10771            Vec::new(),
10772        );
10773        let png = oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap();
10774        let pixmap = tiny_skia::Pixmap::decode_png(&png).unwrap();
10775        for (x, y) in [(74, 87), (150, 37)] {
10776            let pixel = pixmap.pixel(x, y).unwrap();
10777            assert_ne!((pixel.red(), pixel.green(), pixel.blue()), (255, 255, 255));
10778        }
10779    }
10780
10781    #[test]
10782    fn unstyled_four_series_use_accent_one_through_four() {
10783        let chart = four_series_bar_chart();
10784        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10785        let colours = path_colours(&geometry);
10786
10787        assert_eq!(
10788            &colours[..4],
10789            &[
10790                Color::from_hex("156082"),
10791                Color::from_hex("E97132"),
10792                Color::from_hex("196B24"),
10793                Color::from_hex("0F9ED5"),
10794            ]
10795        );
10796    }
10797
10798    #[test]
10799    fn direct_series_solid_colour_overrides_theme_accent() {
10800        for (paint, expected) in [
10801            (
10802                "<a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill>",
10803                Color::from_hex("123456"),
10804            ),
10805            (
10806                "<a:ln><a:solidFill><a:srgbClr val=\"654321\"/></a:solidFill></a:ln>",
10807                Color::from_hex("654321"),
10808            ),
10809            (
10810                "<a:noFill/>",
10811                Color {
10812                    r: 0.0,
10813                    g: 0.0,
10814                    b: 0.0,
10815                    a: 0.0,
10816                },
10817            ),
10818        ] {
10819            let styled =
10820                plot_series(0).replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
10821            let plot = bar_plot("").replace(&plot_series(0), &styled);
10822            let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10823            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10824
10825            assert_eq!(path_colours(&geometry)[0], expected);
10826        }
10827
10828        let styled = plot_series(0).replace(
10829            "<q:cat>",
10830            "<q:spPr><a:solidFill><a:srgbClr val=\"123456\"/></a:solidFill></q:spPr><q:cat>",
10831        );
10832        let plot = bar_plot("").replace(&plot_series(0), &styled);
10833        let xml = chart_with_plot(&plot).replace(
10834            "</q:plotArea></q:chart>",
10835            "</q:plotArea><q:legend/></q:chart>",
10836        );
10837        let chart = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
10838        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
10839        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
10840        assert!(group.children.iter().any(|element| matches!(
10841            element,
10842            PositionedElement::Path(path)
10843                if path.fill == Some(Paint::Solid(Color::from_hex("123456")))
10844        )));
10845
10846        let mut theme = CT_OfficeStyleSheet::office_default();
10847        let preset = oxml_drawing::fill::Fill::from_xml(
10848            br#"<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:prstClr val="red"/></a:solidFill>"#,
10849        )
10850        .unwrap();
10851        let oxml_drawing::fill::Fill::Solid(preset) = preset else {
10852            panic!("expected solid fill");
10853        };
10854        theme.theme_elements.color_scheme.hyperlink = preset.color.unwrap();
10855        let geometry =
10856            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &ColorMap::default())
10857                .unwrap();
10858        assert_eq!(path_colours(&geometry)[0], Color::from_hex("123456"));
10859
10860        for (paint, expected_alpha) in [
10861            ("<a:noFill/>", 0.0),
10862            (
10863                "<a:solidFill><a:srgbClr val=\"123456\"><a:alpha val=\"50000\"/></a:srgbClr></a:solidFill>",
10864                (128.0 / 255.0) * 0.55,
10865            ),
10866        ] {
10867            let (_, area, _) = remaining_plot_fixtures()
10868                .into_iter()
10869                .find(|(kind, _, _)| *kind == "areaChart")
10870                .unwrap();
10871            let styled_area = area.replace("<q:cat>", &format!("<q:spPr>{paint}</q:spPr><q:cat>"));
10872            let chart =
10873                CT_ChartSpace::from_xml(chart_with_optional_axes(&styled_area, true).as_bytes())
10874                    .unwrap();
10875            let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10876            let actual_alpha = path_colours(&geometry)[0].a;
10877
10878            assert!((actual_alpha - expected_alpha).abs() < 1.0e-12);
10879        }
10880    }
10881
10882    #[test]
10883    fn series_accent_cycle_repeats_after_six() {
10884        let chart = seven_series_bar_chart();
10885        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10886        let colours = path_colours(&geometry);
10887
10888        assert_eq!(colours[0], Color::from_hex("156082"));
10889        assert_eq!(colours[6], colours[0]);
10890    }
10891
10892    #[test]
10893    fn series_colours_honor_colour_map_and_transform_order() {
10894        let styled = plot_series(0).replace(
10895            "<q:cat>",
10896            "<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>",
10897        );
10898        let plot = bar_plot("").replace(&plot_series(0), &styled);
10899        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10900        let color_map =
10901            ColorMap::default().with_overrides(&[(ColorMapSlot::Accent1, ThemeColorSlot::Accent4)]);
10902        let mut theme = CT_OfficeStyleSheet::office_default();
10903        let theme_fill = oxml_drawing::fill::Fill::from_xml(
10904            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>"#,
10905        )
10906        .unwrap();
10907        let oxml_drawing::fill::Fill::Solid(theme_fill) = theme_fill else {
10908            panic!("expected solid fill");
10909        };
10910        let theme_colour = theme_fill.color.unwrap();
10911        theme.theme_elements.color_scheme.accent4 = theme_colour.clone();
10912        let geometry =
10913            render_geometry_with_theme(&chart.chart, chart_bounds(), &theme, &color_map).unwrap();
10914        let series_fill = oxml_drawing::fill::Fill::from_xml(
10915            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>"#,
10916        )
10917        .unwrap();
10918        let oxml_drawing::fill::Fill::Solid(series_fill) = series_fill else {
10919            panic!("expected solid fill");
10920        };
10921        let mut transforms = theme_colour.transforms().to_vec();
10922        transforms.extend_from_slice(series_fill.color.as_ref().unwrap().transforms());
10923        let expected = oxml_drawing::color::apply_color_transforms(
10924            RgbColor::parse("336699").unwrap(),
10925            &transforms,
10926        );
10927
10928        assert_eq!(path_colours(&geometry)[0], resolved_layout_colour(expected));
10929    }
10930
10931    #[test]
10932    fn chart_colours_do_not_use_word_tint_shade() {
10933        let styled = plot_series(0).replace(
10934            "<q:cat>",
10935            "<q:spPr><a:solidFill><a:srgbClr val=\"808080\"><a:tint val=\"50000\"/></a:srgbClr></a:solidFill></q:spPr><q:cat>",
10936        );
10937        let plot = bar_plot("").replace(&plot_series(0), &styled);
10938        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10939        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10940
10941        assert_eq!(
10942            path_colours(&geometry)[0],
10943            Color {
10944                r: 205.0 / 255.0,
10945                g: 205.0 / 255.0,
10946                b: 205.0 / 255.0,
10947                a: 1.0,
10948            }
10949        );
10950    }
10951
10952    #[test]
10953    fn unsupported_direct_series_paint_is_contextual() {
10954        let styled = plot_series(0).replace(
10955            "<q:cat>",
10956            "<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>",
10957        );
10958        let plot = bar_plot("").replace(&plot_series(0), &styled);
10959        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
10960        let error = render_geometry(&chart.chart, chart_bounds()).unwrap_err();
10961
10962        assert!(error.to_string().contains("c:ser[0] direct fill paint"));
10963        assert!(error.to_string().contains("gradient"));
10964    }
10965
10966    #[test]
10967    fn resolved_chart_palette_raster_is_deterministic() {
10968        let raster = || {
10969            let chart = four_series_bar_chart();
10970            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
10971            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
10972            let layout = LayoutResult::new(
10973                vec![PageFrame::new(
10974                    1,
10975                    200.0,
10976                    140.0,
10977                    vec![PositionedElement::Group(group)],
10978                )],
10979                fonts.all_font_data(),
10980                None,
10981                Vec::new(),
10982            );
10983            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
10984        };
10985        let chart = four_series_bar_chart();
10986        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
10987
10988        assert_eq!(path_colours(&geometry)[0], Color::from_hex("156082"));
10989        assert_eq!(raster(), raster());
10990    }
10991
10992    #[test]
10993    fn bar_geometry_handles_direction_grouping_gap_and_overlap() {
10994        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
10995        let Plot::Bar {
10996            overlap, series, ..
10997        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
10998        else {
10999            panic!("expected bar plot");
11000        };
11001        let mut second = series[0].clone();
11002        second.index = 1;
11003        second.order = 1;
11004        second.values.values = vec![2.0, 1.0];
11005        series.push(second);
11006
11007        *overlap = 25;
11008        let clustered = render_geometry(&chart.chart, chart_bounds()).unwrap();
11009        let clustered_bounds = path_bounds(&clustered);
11010        let clustered_width = 30.4 / 1.75;
11011        let clustered_advance = clustered_width * 0.75;
11012        assert_rects_near(
11013            &clustered_bounds,
11014            &[
11015                Rect {
11016                    x: 58.8,
11017                    y: 62.0,
11018                    width: clustered_width,
11019                    height: 50.0,
11020                },
11021                Rect {
11022                    x: 134.8,
11023                    y: 12.0,
11024                    width: clustered_width,
11025                    height: 100.0,
11026                },
11027                Rect {
11028                    x: 58.8 + clustered_advance,
11029                    y: 12.0,
11030                    width: clustered_width,
11031                    height: 100.0,
11032                },
11033                Rect {
11034                    x: 134.8 + clustered_advance,
11035                    y: 62.0,
11036                    width: clustered_width,
11037                    height: 50.0,
11038                },
11039            ],
11040        );
11041
11042        let Plot::Bar { direction, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11043            unreachable!()
11044        };
11045        *direction = BarDirection::Bar;
11046        let horizontal = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11047        let horizontal_width = 20.0 / 1.75;
11048        let horizontal_advance = horizontal_width * 0.75;
11049        assert_rects_near(
11050            &horizontal,
11051            &[
11052                Rect {
11053                    x: 36.0,
11054                    y: 27.0,
11055                    width: 76.0,
11056                    height: horizontal_width,
11057                },
11058                Rect {
11059                    x: 36.0,
11060                    y: 77.0,
11061                    width: 152.0,
11062                    height: horizontal_width,
11063                },
11064                Rect {
11065                    x: 36.0,
11066                    y: 27.0 + horizontal_advance,
11067                    width: 152.0,
11068                    height: horizontal_width,
11069                },
11070                Rect {
11071                    x: 36.0,
11072                    y: 77.0 + horizontal_advance,
11073                    width: 76.0,
11074                    height: horizontal_width,
11075                },
11076            ],
11077        );
11078
11079        let Plot::Bar {
11080            direction,
11081            grouping,
11082            ..
11083        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11084        else {
11085            unreachable!()
11086        };
11087        *direction = BarDirection::Column;
11088        *grouping = BarGrouping::Stacked;
11089        let stacked = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11090        assert_rects_near(
11091            &stacked,
11092            &[
11093                Rect {
11094                    x: 58.8,
11095                    y: 112.0 - 100.0 / 3.0,
11096                    width: 30.4,
11097                    height: 100.0 / 3.0,
11098                },
11099                Rect {
11100                    x: 134.8,
11101                    y: 112.0 - 200.0 / 3.0,
11102                    width: 30.4,
11103                    height: 200.0 / 3.0,
11104                },
11105                Rect {
11106                    x: 58.8,
11107                    y: 12.0,
11108                    width: 30.4,
11109                    height: 200.0 / 3.0,
11110                },
11111                Rect {
11112                    x: 134.8,
11113                    y: 12.0,
11114                    width: 30.4,
11115                    height: 100.0 / 3.0,
11116                },
11117            ],
11118        );
11119
11120        let Plot::Bar {
11121            grouping, series, ..
11122        } = &mut chart.chart.plot_area.plots_mut().unwrap()[0]
11123        else {
11124            unreachable!()
11125        };
11126        *grouping = BarGrouping::PercentStacked;
11127        series[1].values.values = vec![2.0, 2.0];
11128        let percent = path_bounds(&render_geometry(&chart.chart, chart_bounds()).unwrap());
11129        assert_rects_near(
11130            &percent,
11131            &[
11132                Rect {
11133                    x: 58.8,
11134                    y: 112.0 - 100.0 / 3.0,
11135                    width: 30.4,
11136                    height: 100.0 / 3.0,
11137                },
11138                Rect {
11139                    x: 134.8,
11140                    y: 62.0,
11141                    width: 30.4,
11142                    height: 50.0,
11143                },
11144                Rect {
11145                    x: 58.8,
11146                    y: 12.0,
11147                    width: 30.4,
11148                    height: 200.0 / 3.0,
11149                },
11150                Rect {
11151                    x: 134.8,
11152                    y: 12.0,
11153                    width: 30.4,
11154                    height: 50.0,
11155                },
11156            ],
11157        );
11158    }
11159
11160    #[test]
11161    fn line_scatter_and_radar_emit_paths_and_markers() {
11162        let line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11163        let line_geometry = render_geometry(&line.chart, chart_bounds()).unwrap();
11164        assert_path_points(
11165            path_commands(&line_geometry)[0],
11166            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11167            false,
11168        );
11169        assert_rects_near(
11170            &path_bounds(&line_geometry)[1..],
11171            &[
11172                Rect {
11173                    x: 71.5,
11174                    y: 109.5,
11175                    width: 5.0,
11176                    height: 5.0,
11177                },
11178                Rect {
11179                    x: 147.5,
11180                    y: 9.5,
11181                    width: 5.0,
11182                    height: 5.0,
11183                },
11184            ],
11185        );
11186
11187        let (scatter_plot, _) = remaining_plot_fixtures()
11188            .into_iter()
11189            .find(|(kind, _, _)| *kind == "scatterChart")
11190            .map(|(_, plot, axes)| (plot, axes))
11191            .unwrap();
11192        let mut scatter =
11193            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11194                .unwrap();
11195        let Plot::Scatter { style, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11196        else {
11197            unreachable!()
11198        };
11199        *style = ScatterStyle::LineMarker;
11200        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11201        assert_path_points(
11202            path_commands(&scatter_geometry)[0],
11203            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11204            false,
11205        );
11206        assert_rects_near(
11207            &path_bounds(&scatter_geometry)[1..],
11208            &[
11209                Rect {
11210                    x: 33.5,
11211                    y: 109.5,
11212                    width: 5.0,
11213                    height: 5.0,
11214                },
11215                Rect {
11216                    x: 185.5,
11217                    y: 9.5,
11218                    width: 5.0,
11219                    height: 5.0,
11220                },
11221            ],
11222        );
11223
11224        let (radar_plot, _) = remaining_plot_fixtures()
11225            .into_iter()
11226            .find(|(kind, _, _)| *kind == "radarChart")
11227            .map(|(_, plot, axes)| (plot, axes))
11228            .unwrap();
11229        let radar = CT_ChartSpace::from_xml(chart_with_optional_axes(&radar_plot, true).as_bytes())
11230            .unwrap();
11231        let radar_geometry = render_geometry(&radar.chart, chart_bounds()).unwrap();
11232        assert_path_points(
11233            path_commands(&radar_geometry)[0],
11234            &[Point { x: 112.0, y: 37.0 }, Point { x: 112.0, y: 112.0 }],
11235            true,
11236        );
11237        assert_rects_near(
11238            &path_bounds(&radar_geometry)[1..],
11239            &[
11240                Rect {
11241                    x: 109.5,
11242                    y: 34.5,
11243                    width: 5.0,
11244                    height: 5.0,
11245                },
11246                Rect {
11247                    x: 109.5,
11248                    y: 109.5,
11249                    width: 5.0,
11250                    height: 5.0,
11251                },
11252            ],
11253        );
11254    }
11255
11256    #[test]
11257    fn pie_doughnut_and_area_emit_closed_paths() {
11258        let fixtures = remaining_plot_fixtures();
11259        let pie_plot = fixtures
11260            .iter()
11261            .find(|(kind, _, _)| *kind == "pieChart")
11262            .unwrap();
11263        let pie =
11264            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot.1, pie_plot.2).as_bytes())
11265                .unwrap();
11266        let pie_geometry = render_geometry(&pie.chart, chart_bounds()).unwrap();
11267        let pie_commands = path_commands(&pie_geometry);
11268        assert_eq!(pie_commands.len(), 2);
11269        let [
11270            PathCommand::MoveTo(center),
11271            PathCommand::LineTo(start),
11272            ..,
11273            PathCommand::CurveTo { to: end, .. },
11274            PathCommand::Close,
11275        ] = pie_commands[0]
11276        else {
11277            panic!("expected closed cubic pie wedge");
11278        };
11279        assert_point_near(*center, Point { x: 112.0, y: 62.0 });
11280        assert_point_near(
11281            *start,
11282            Point {
11283                x: 137.0,
11284                y: 62.0 - 25.0 * 3.0_f64.sqrt(),
11285            },
11286        );
11287        assert_point_near(
11288            *end,
11289            Point {
11290                x: 137.0,
11291                y: 62.0 + 25.0 * 3.0_f64.sqrt(),
11292            },
11293        );
11294
11295        let doughnut_plot = fixtures
11296            .iter()
11297            .find(|(kind, _, _)| *kind == "doughnutChart")
11298            .unwrap();
11299        let doughnut = CT_ChartSpace::from_xml(
11300            chart_with_optional_axes(&doughnut_plot.1, doughnut_plot.2).as_bytes(),
11301        )
11302        .unwrap();
11303        let doughnut_geometry = render_geometry(&doughnut.chart, chart_bounds()).unwrap();
11304        let doughnut_commands = path_commands(&doughnut_geometry);
11305        assert_eq!(doughnut_commands.len(), 2);
11306        let commands = doughnut_commands[0];
11307        assert_eq!(commands.len(), 7);
11308        let PathCommand::MoveTo(outer_start) = commands[0] else {
11309            panic!("expected doughnut outer start");
11310        };
11311        let PathCommand::LineTo(inner_end) = commands[3] else {
11312            panic!("expected doughnut inner-radius join");
11313        };
11314        let PathCommand::CurveTo {
11315            to: inner_start, ..
11316        } = commands[5]
11317        else {
11318            panic!("expected reverse inner arc");
11319        };
11320        let diagonal = 25.0 * 2.0_f64.sqrt();
11321        assert_point_near(
11322            outer_start,
11323            Point {
11324                x: 112.0 + diagonal,
11325                y: 62.0 - diagonal,
11326            },
11327        );
11328        assert_point_near(
11329            inner_end,
11330            Point {
11331                x: 112.0 + 30.0 * 75.0_f64.to_radians().cos(),
11332                y: 62.0 + 30.0 * 75.0_f64.to_radians().sin(),
11333            },
11334        );
11335        let inner_diagonal = 15.0 * 2.0_f64.sqrt();
11336        assert_point_near(
11337            inner_start,
11338            Point {
11339                x: 112.0 + inner_diagonal,
11340                y: 62.0 - inner_diagonal,
11341            },
11342        );
11343        assert_eq!(commands.last(), Some(&PathCommand::Close));
11344
11345        let area_plot = fixtures
11346            .iter()
11347            .find(|(kind, _, _)| *kind == "areaChart")
11348            .unwrap();
11349        let mut area =
11350            CT_ChartSpace::from_xml(chart_with_optional_axes(&area_plot.1, area_plot.2).as_bytes())
11351                .unwrap();
11352        let area_geometry = render_geometry(&area.chart, chart_bounds()).unwrap();
11353        assert_path_points(
11354            path_commands(&area_geometry)[0],
11355            &[
11356                Point { x: 74.0, y: 62.0 },
11357                Point { x: 150.0, y: 12.0 },
11358                Point { x: 150.0, y: 112.0 },
11359                Point { x: 74.0, y: 112.0 },
11360            ],
11361            true,
11362        );
11363
11364        let Plot::Area {
11365            grouping, series, ..
11366        } = &mut area.chart.plot_area.plots_mut().unwrap()[0]
11367        else {
11368            unreachable!()
11369        };
11370        *grouping = Grouping::Stacked;
11371        let mut second = series[0].clone();
11372        second.index = 1;
11373        second.order = 1;
11374        second.values.values = vec![2.0, 1.0];
11375        series.push(second);
11376        let stacked_area = render_geometry(&area.chart, chart_bounds()).unwrap();
11377        assert_path_points(
11378            path_commands(&stacked_area)[1],
11379            &[
11380                Point { x: 74.0, y: 12.0 },
11381                Point { x: 150.0, y: 12.0 },
11382                Point {
11383                    x: 150.0,
11384                    y: 112.0 - 200.0 / 3.0,
11385                },
11386                Point {
11387                    x: 74.0,
11388                    y: 112.0 - 100.0 / 3.0,
11389                },
11390            ],
11391            true,
11392        );
11393    }
11394
11395    #[test]
11396    fn sparse_cache_indexes_preserve_slots_and_scatter_pairing() {
11397        let series = sparse_category_series();
11398        let bar = format!(
11399            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>"#
11400        );
11401        let bar = CT_ChartSpace::from_xml(chart_with_plot(&bar).as_bytes()).unwrap();
11402        assert_rects_near(
11403            &path_bounds(&render_geometry(&bar.chart, chart_bounds()).unwrap()),
11404            &[
11405                Rect {
11406                    x: 51.2,
11407                    y: 62.0,
11408                    width: 304.0 / 15.0,
11409                    height: 50.0,
11410                },
11411                Rect {
11412                    x: 36.0 + 304.0 / 3.0 + 15.2,
11413                    y: 12.0,
11414                    width: 304.0 / 15.0,
11415                    height: 100.0,
11416                },
11417            ],
11418        );
11419
11420        let line = format!(
11421            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>"#
11422        );
11423        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line).as_bytes()).unwrap();
11424        let first = Point {
11425            x: 36.0 + 76.0 / 3.0,
11426            y: 62.0,
11427        };
11428        let missing = Point { x: 112.0, y: 112.0 };
11429        let last = Point {
11430            x: 36.0 + 380.0 / 3.0,
11431            y: 12.0,
11432        };
11433        let gap = render_geometry(&line.chart, chart_bounds()).unwrap();
11434        let gap_commands = path_commands(&gap);
11435        assert_eq!(gap_commands.len(), 2);
11436        assert_path_points(
11437            gap_commands[0],
11438            &[Point {
11439                x: first.x,
11440                y: 112.0,
11441            }],
11442            false,
11443        );
11444        assert_path_points(gap_commands[1], &[last], false);
11445        line.chart.disp_blanks_as = DispBlanksAs::Zero;
11446        assert_path_points(
11447            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11448            &[first, missing, last],
11449            false,
11450        );
11451        line.chart.disp_blanks_as = DispBlanksAs::Span;
11452        assert_path_points(
11453            path_commands(&render_geometry(&line.chart, chart_bounds()).unwrap())[0],
11454            &[
11455                Point {
11456                    x: first.x,
11457                    y: 112.0,
11458                },
11459                last,
11460            ],
11461            false,
11462        );
11463
11464        let area = format!(
11465            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:areaChart>"#
11466        );
11467        let mut area = CT_ChartSpace::from_xml(chart_with_plot(&area).as_bytes()).unwrap();
11468        let area_gap = render_geometry(&area.chart, chart_bounds()).unwrap();
11469        let area_gap_commands = path_commands(&area_gap);
11470        assert_eq!(area_gap_commands.len(), 2);
11471        assert_path_points(
11472            area_gap_commands[0],
11473            &[
11474                first,
11475                Point {
11476                    x: first.x,
11477                    y: 112.0,
11478                },
11479            ],
11480            true,
11481        );
11482        assert_path_points(
11483            area_gap_commands[1],
11484            &[
11485                last,
11486                Point {
11487                    x: last.x,
11488                    y: 112.0,
11489                },
11490            ],
11491            true,
11492        );
11493        area.chart.disp_blanks_as = DispBlanksAs::Zero;
11494        assert_path_points(
11495            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11496            &[
11497                first,
11498                missing,
11499                last,
11500                Point {
11501                    x: last.x,
11502                    y: 112.0,
11503                },
11504                missing,
11505                Point {
11506                    x: first.x,
11507                    y: 112.0,
11508                },
11509            ],
11510            true,
11511        );
11512        area.chart.disp_blanks_as = DispBlanksAs::Span;
11513        assert_path_points(
11514            path_commands(&render_geometry(&area.chart, chart_bounds()).unwrap())[0],
11515            &[
11516                first,
11517                last,
11518                Point {
11519                    x: last.x,
11520                    y: 112.0,
11521                },
11522                Point {
11523                    x: first.x,
11524                    y: 112.0,
11525                },
11526            ],
11527            true,
11528        );
11529
11530        let radar = format!(
11531            r#"<q:radarChart><q:radarStyle val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#
11532        );
11533        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
11534        assert_path_points(
11535            path_commands(&render_geometry(&radar.chart, chart_bounds()).unwrap())[0],
11536            &[
11537                Point { x: 112.0, y: 37.0 },
11538                Point {
11539                    x: 112.0 - 25.0 * 3.0_f64.sqrt(),
11540                    y: 87.0,
11541                },
11542            ],
11543            true,
11544        );
11545
11546        let scatter = format!(
11547            r#"<q:scatterChart><q:scatterStyle val="lineMarker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:scatterChart>"#,
11548            sparse_scatter_series()
11549        );
11550        let mut scatter = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11551        let scatter_geometry = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11552        assert_eq!(geometry_children(&scatter_geometry).len(), 4);
11553        let gap_commands = path_commands(&scatter_geometry);
11554        assert_path_points(gap_commands[0], &[Point { x: 36.0, y: 112.0 }], false);
11555        assert_path_points(gap_commands[1], &[Point { x: 188.0, y: 12.0 }], false);
11556
11557        scatter.chart.disp_blanks_as = DispBlanksAs::Zero;
11558        let zero = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11559        assert_path_points(
11560            path_commands(&zero)[0],
11561            &[
11562                Point { x: 74.0, y: 87.0 },
11563                Point { x: 36.0, y: 62.0 },
11564                Point { x: 150.0, y: 112.0 },
11565                Point { x: 188.0, y: 12.0 },
11566            ],
11567            false,
11568        );
11569
11570        scatter.chart.disp_blanks_as = DispBlanksAs::Span;
11571        assert_path_points(
11572            path_commands(&render_geometry(&scatter.chart, chart_bounds()).unwrap())[0],
11573            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11574            false,
11575        );
11576    }
11577
11578    #[test]
11579    fn sparse_scatter_domains_and_labels_use_only_matched_points() {
11580        let scatter = format!(
11581            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>"#,
11582            sparse_scatter_series()
11583        );
11584        let chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11585        let plot = &chart.chart.plot_area.plots().unwrap()[0];
11586        assert_eq!(
11587            super::scatter_domains(plot, DispBlanksAs::Gap).unwrap(),
11588            (
11589                Domain {
11590                    min: 10.0,
11591                    max: 40.0,
11592                },
11593                Domain {
11594                    min: 100.0,
11595                    max: 400.0,
11596                },
11597            )
11598        );
11599        let axes = chart.chart.plot_area.axes().unwrap();
11600        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
11601        let anchors =
11602            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
11603        assert_eq!(
11604            anchors.keys().copied().collect::<Vec<_>>(),
11605            vec![(0, 0), (0, 3)]
11606        );
11607        assert_eq!(anchors[&(0, 0)].center, Point { x: 0.0, y: 140.0 });
11608        assert_eq!(anchors[&(0, 3)].center, Point { x: 200.0, y: 0.0 });
11609    }
11610
11611    #[test]
11612    fn sparse_bubble_size_labels_resolve_logical_indexes() {
11613        let scatter = format!(
11614            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>"#,
11615            sparse_scatter_series()
11616        );
11617        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&scatter).as_bytes()).unwrap();
11618        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
11619        let Plot::Scatter { series, .. } = plot else {
11620            panic!("expected scatter plot");
11621        };
11622        series[0].bubble_size = Some(
11623            NumericData::from_xml(
11624                format!(
11625                    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>"#
11626                )
11627                .as_bytes(),
11628            )
11629            .unwrap(),
11630        );
11631        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11632        let mut rendered = Vec::new();
11633        super::render_data_labels(
11634            plot,
11635            chart_bounds(),
11636            DispBlanksAs::Gap,
11637            super::GeometryOptions::default(),
11638            &mut fonts,
11639            &mut rendered,
11640        )
11641        .unwrap();
11642        let text: Vec<_> = rendered
11643            .iter()
11644            .filter_map(|element| match element {
11645                PositionedElement::Text(run) => Some(run.text.as_str()),
11646                _ => None,
11647            })
11648            .collect();
11649        assert_eq!(text, vec!["5", "9"]);
11650    }
11651
11652    #[test]
11653    fn geometry_rejects_invalid_bounds_and_opaque_plots() {
11654        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11655        for bounds in [
11656            Rect {
11657                x: 0.0,
11658                y: 0.0,
11659                width: f64::NAN,
11660                height: 100.0,
11661            },
11662            Rect {
11663                x: 0.0,
11664                y: 0.0,
11665                width: 0.0,
11666                height: 100.0,
11667            },
11668            Rect {
11669                x: 0.0,
11670                y: 0.0,
11671                width: 40.0,
11672                height: 30.0,
11673            },
11674        ] {
11675            assert!(render_geometry(&chart.chart, bounds).is_err());
11676        }
11677
11678        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11679            unreachable!()
11680        };
11681        series[0].values.values.clear();
11682        assert!(render_geometry(&chart.chart, chart_bounds()).is_err());
11683
11684        let opaque = CT_ChartSpace::from_xml(
11685            chart_with_plot(r#"<q:bar3DChart><q:barDir val="col"/></q:bar3DChart>"#).as_bytes(),
11686        )
11687        .unwrap();
11688        let error = render_geometry(&opaque.chart, chart_bounds()).unwrap_err();
11689        assert!(error.to_string().contains("unsupported or combination"));
11690
11691        let (scatter_plot, _) = remaining_plot_fixtures()
11692            .into_iter()
11693            .find(|(kind, _, _)| *kind == "scatterChart")
11694            .map(|(_, plot, axes)| (plot, axes))
11695            .unwrap();
11696        let mut scatter =
11697            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11698                .unwrap();
11699        for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
11700            let Plot::Scatter { series, .. } = &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11701            else {
11702                unreachable!()
11703            };
11704            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11705                unreachable!()
11706            };
11707            x_values.values[0] = invalid;
11708            let error = render_geometry(&scatter.chart, chart_bounds()).unwrap_err();
11709            assert!(error.to_string().contains("xVal"));
11710        }
11711    }
11712
11713    #[test]
11714    fn finite_extremes_never_produce_nonfinite_geometry() {
11715        let mut line = CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11716        let Plot::Line { series, .. } = &mut line.chart.plot_area.plots_mut().unwrap()[0] else {
11717            unreachable!()
11718        };
11719        series[0].values.values = vec![-f64::MAX, f64::MAX];
11720        let extreme_line = render_geometry(&line.chart, chart_bounds()).unwrap();
11721        assert_path_points(
11722            path_commands(&extreme_line)[0],
11723            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11724            false,
11725        );
11726
11727        let mut stacked =
11728            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11729        let Plot::Bar {
11730            grouping, series, ..
11731        } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11732        else {
11733            unreachable!()
11734        };
11735        *grouping = BarGrouping::Stacked;
11736        series[0].values.values = vec![f64::MAX, 1.0];
11737        let mut second = series[0].clone();
11738        second.index = 1;
11739        second.order = 1;
11740        series.push(second);
11741        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11742        assert!(error.to_string().contains("stacked value total"));
11743
11744        let Plot::Bar { grouping, .. } = &mut stacked.chart.plot_area.plots_mut().unwrap()[0]
11745        else {
11746            unreachable!()
11747        };
11748        *grouping = BarGrouping::PercentStacked;
11749        let error = render_geometry(&stacked.chart, chart_bounds()).unwrap_err();
11750        assert!(error.to_string().contains("stacked percentage total"));
11751
11752        let (pie_plot, _) = remaining_plot_fixtures()
11753            .into_iter()
11754            .find(|(kind, _, _)| *kind == "pieChart")
11755            .map(|(_, plot, axes)| (plot, axes))
11756            .unwrap();
11757        let mut pie =
11758            CT_ChartSpace::from_xml(chart_with_optional_axes(&pie_plot, false).as_bytes()).unwrap();
11759        let Plot::Pie { series, .. } = &mut pie.chart.plot_area.plots_mut().unwrap()[0] else {
11760            unreachable!()
11761        };
11762        series[0].values.values = vec![f64::MAX, f64::MAX];
11763        let error = render_geometry(&pie.chart, chart_bounds()).unwrap_err();
11764        assert!(error.to_string().contains("pie value total"));
11765
11766        let (scatter_plot, _) = remaining_plot_fixtures()
11767            .into_iter()
11768            .find(|(kind, _, _)| *kind == "scatterChart")
11769            .map(|(_, plot, axes)| (plot, axes))
11770            .unwrap();
11771        let mut scatter =
11772            CT_ChartSpace::from_xml(chart_with_optional_axes(&scatter_plot, true).as_bytes())
11773                .unwrap();
11774        let Plot::Scatter { style, series, .. } =
11775            &mut scatter.chart.plot_area.plots_mut().unwrap()[0]
11776        else {
11777            unreachable!()
11778        };
11779        *style = ScatterStyle::Line;
11780        let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
11781            unreachable!()
11782        };
11783        x_values.values = vec![-f64::MAX, f64::MAX];
11784        series[0].values.values = vec![-f64::MAX, f64::MAX];
11785        let extreme_scatter = render_geometry(&scatter.chart, chart_bounds()).unwrap();
11786        assert_path_points(
11787            path_commands(&extreme_scatter)[0],
11788            &[Point { x: 36.0, y: 112.0 }, Point { x: 188.0, y: 12.0 }],
11789            false,
11790        );
11791    }
11792
11793    #[test]
11794    fn geometry_is_backend_neutral_and_deterministic() {
11795        let chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11796        let first = render_geometry(&chart.chart, chart_bounds()).unwrap();
11797        let second = render_geometry(&chart.chart, chart_bounds()).unwrap();
11798        assert_eq!(
11799            format!("{:?}", first.elements),
11800            format!("{:?}", second.elements)
11801        );
11802
11803        let raster = |geometry: ChartGeometry| {
11804            let layout = LayoutResult::new(
11805                vec![PageFrame::new(1, 200.0, 140.0, geometry.elements)],
11806                Vec::new(),
11807                None,
11808                Vec::new(),
11809            );
11810            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
11811        };
11812        assert_eq!(raster(first), raster(second));
11813    }
11814
11815    #[test]
11816    fn zero_to_one_hundred_axis_uses_expected_ticks() {
11817        let scale = nice_number_scale(
11818            Domain {
11819                min: 0.0,
11820                max: 100.0,
11821            },
11822            None,
11823            None,
11824            6,
11825        )
11826        .unwrap();
11827        assert_eq!(scale.ticks, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
11828    }
11829
11830    #[test]
11831    fn nice_number_ticks_cover_unpinned_extents() {
11832        for domain in [
11833            Domain {
11834                min: 3.0,
11835                max: 97.0,
11836            },
11837            Domain {
11838                min: -97.0,
11839                max: -3.0,
11840            },
11841            Domain {
11842                min: -3.5,
11843                max: 8.25,
11844            },
11845            Domain {
11846                min: 0.125,
11847                max: 0.875,
11848            },
11849            Domain {
11850                min: 1.0e12,
11851                max: 7.0e12,
11852            },
11853            Domain {
11854                min: -f64::MAX,
11855                max: f64::MAX,
11856            },
11857            Domain {
11858                min: f64::from_bits(1),
11859                max: f64::from_bits(5),
11860            },
11861        ] {
11862            let scale = nice_number_scale(domain, None, None, 6).unwrap();
11863            assert!(scale.domain.min <= domain.min);
11864            assert!(scale.domain.max >= domain.max);
11865            assert!(scale.ticks.iter().all(|tick| tick.is_finite()));
11866            assert!(scale.ticks.windows(2).all(|pair| pair[0] < pair[1]));
11867        }
11868
11869        let constant = nice_number_scale(Domain { min: 4.0, max: 4.0 }, None, None, 6).unwrap();
11870        assert!(constant.domain.min < 4.0 && constant.domain.max > 4.0);
11871    }
11872
11873    #[test]
11874    fn one_sided_explicit_scale_beyond_data_derives_a_valid_opposite_bound() {
11875        let minimum = nice_number_scale(
11876            Domain {
11877                min: 0.0,
11878                max: 10.0,
11879            },
11880            Some(20.0),
11881            None,
11882            6,
11883        )
11884        .unwrap();
11885        assert_eq!(minimum.domain.min, 20.0);
11886        assert!(minimum.domain.max > 20.0);
11887        assert!(minimum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
11888
11889        let maximum = nice_number_scale(
11890            Domain {
11891                min: 0.0,
11892                max: 10.0,
11893            },
11894            None,
11895            Some(-20.0),
11896            6,
11897        )
11898        .unwrap();
11899        assert_eq!(maximum.domain.max, -20.0);
11900        assert!(maximum.domain.min < -20.0);
11901        assert!(maximum.ticks.windows(2).all(|pair| pair[0] < pair[1]));
11902    }
11903
11904    #[test]
11905    fn standard_line_domain_does_not_force_zero() {
11906        let mut chart =
11907            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
11908        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
11909        let Plot::Line { series, .. } = plot else {
11910            panic!("expected line plot");
11911        };
11912        series[0].values.values = vec![1_000.0, 1_010.0];
11913        assert_eq!(
11914            super::plot_value_domain(plot, DispBlanksAs::Gap).unwrap(),
11915            Some(Domain {
11916                min: 1_000.0,
11917                max: 1_010.0,
11918            })
11919        );
11920        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
11921        assert_path_points(
11922            path_commands(&geometry)[0],
11923            &[Point { x: 74.0, y: 112.0 }, Point { x: 150.0, y: 12.0 }],
11924            false,
11925        );
11926    }
11927
11928    #[test]
11929    fn explicit_scaling_bounds_and_orientation_are_honored() {
11930        let scale = nice_number_scale(
11931            Domain {
11932                min: 25.0,
11933                max: 75.0,
11934            },
11935            Some(0.0),
11936            Some(100.0),
11937            6,
11938        )
11939        .unwrap();
11940        assert_eq!(
11941            scale.domain,
11942            Domain {
11943                min: 0.0,
11944                max: 100.0
11945            }
11946        );
11947        assert_eq!(
11948            super::map_y_oriented(0.0, scale.domain, chart_bounds(), Orientation::MaxMin).unwrap(),
11949            0.0
11950        );
11951        assert_eq!(
11952            super::map_y_oriented(100.0, scale.domain, chart_bounds(), Orientation::MaxMin)
11953                .unwrap(),
11954            140.0
11955        );
11956
11957        let mut chart = labelled_bar_chart();
11958        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
11959            panic!("expected bar plot");
11960        };
11961        series[0].values.values = vec![25.0, 75.0];
11962        let value_axis = chart
11963            .chart
11964            .plot_area
11965            .axes
11966            .iter_mut()
11967            .find(|axis| axis.kind == AxisKind::Value)
11968            .unwrap();
11969        value_axis.scaling.orientation = Orientation::MaxMin;
11970        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
11971        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
11972        let plot = group
11973            .children
11974            .iter()
11975            .find_map(|element| match element {
11976                PositionedElement::Group(group) => Some(group),
11977                _ => None,
11978            })
11979            .unwrap();
11980        let bars: Vec<_> = plot
11981            .children
11982            .iter()
11983            .filter_map(|element| match element {
11984                PositionedElement::Path(path) => path.path.bounds(),
11985                _ => None,
11986            })
11987            .collect();
11988        assert_eq!(bars.len(), 2);
11989        assert!((bars[0].y - 12.0).abs() < 1.0e-9);
11990        assert!((bars[0].height - 25.0).abs() < 1.0e-9);
11991        assert!((bars[1].height - 75.0).abs() < 1.0e-9);
11992    }
11993
11994    #[test]
11995    fn family_label_anchors_share_rendered_geometry() {
11996        let mut bar = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
11997        let bar_plot = &mut bar.chart.plot_area.plots_mut().unwrap()[0];
11998        let Plot::Bar { series, .. } = bar_plot else {
11999            panic!("expected bar plot");
12000        };
12001        let mut second = series[0].clone();
12002        second.index = 1;
12003        second.order = 1;
12004        series.push(second);
12005        let anchors = super::plot_label_anchors(
12006            bar_plot,
12007            chart_bounds(),
12008            DispBlanksAs::Gap,
12009            super::GeometryOptions::default(),
12010        )
12011        .unwrap();
12012        assert_point_near(anchors[&(0, 0)].center, Point { x: 40.0, y: 105.0 });
12013        assert_point_near(anchors[&(1, 0)].center, Point { x: 60.0, y: 105.0 });
12014
12015        let pie = CT_ChartSpace::from_xml(
12016            chart_with_optional_axes(
12017                &remaining_plot_fixtures()
12018                    .into_iter()
12019                    .find(|(kind, _, _)| *kind == "pieChart")
12020                    .unwrap()
12021                    .1,
12022                false,
12023            )
12024            .as_bytes(),
12025        )
12026        .unwrap();
12027        let Plot::Pie {
12028            first_slice_angle,
12029            series,
12030            ..
12031        } = &pie.chart.plot_area.plots().unwrap()[0]
12032        else {
12033            panic!("expected pie plot");
12034        };
12035        let slices = super::pie_slices(*first_slice_angle, None, series, chart_bounds()).unwrap();
12036        assert_eq!(slices.len(), 2);
12037        let first_span = std::f64::consts::TAU / 3.0;
12038        let first_midpoint = f64::from(*first_slice_angle).to_radians()
12039            - std::f64::consts::FRAC_PI_2
12040            + first_span / 2.0;
12041        assert_point_near(
12042            slices[0].anchor,
12043            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, first_midpoint),
12044        );
12045        let second_midpoint = first_midpoint + std::f64::consts::PI;
12046        assert_point_near(
12047            slices[1].anchor,
12048            super::circle_point(Point { x: 100.0, y: 70.0 }, 140.0 / 3.0, second_midpoint),
12049        );
12050
12051        let radar = format!(
12052            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12053            plot_series(0)
12054        );
12055        let radar = CT_ChartSpace::from_xml(chart_with_plot(&radar).as_bytes()).unwrap();
12056        let Plot::Radar { series, .. } = &radar.chart.plot_area.plots().unwrap()[0] else {
12057            panic!("expected radar plot");
12058        };
12059        let radar_points =
12060            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12061                .unwrap();
12062        assert_eq!(radar_points[0][0], (0, Point { x: 100.0, y: 35.0 }));
12063        assert_eq!(radar_points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12064    }
12065
12066    #[test]
12067    fn radar_annotations_use_spokes_perimeter_labels_and_radial_gridlines() {
12068        let radar_plot = format!(
12069            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12070            sparse_category_series()
12071        );
12072        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12073        for axis in &mut chart.chart.plot_area.axes {
12074            axis.major_tick_mark = TickMark::Cross;
12075            if axis.kind == AxisKind::Value {
12076                axis.major_gridlines = Some(super::ChartLines::default());
12077            }
12078        }
12079        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12080        let axes = chart.chart.plot_area.axes().unwrap();
12081        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12082        let center = Point { x: 112.0, y: 62.0 };
12083        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12084        let annotations =
12085            super::render_axes_and_labels(plot, &axes, bounds, DispBlanksAs::Gap, &mut fonts)
12086                .unwrap();
12087        assert_eq!(annotations.axis_lines.len(), 4);
12088        for spoke in annotations.axis_lines.iter().take(3) {
12089            let PositionedElement::Path(spoke) = spoke else {
12090                panic!("radar spoke must be a path");
12091            };
12092            assert_eq!(
12093                spoke.path.commands.first(),
12094                Some(&PathCommand::MoveTo(center))
12095            );
12096        }
12097        assert!(!annotations.gridlines.is_empty());
12098        for gridline in &annotations.gridlines {
12099            let PositionedElement::Path(gridline) = gridline else {
12100                panic!("radar gridline must be a path");
12101            };
12102            assert_eq!(gridline.path.commands.last(), Some(&PathCommand::Close));
12103            let radii: Vec<_> = gridline
12104                .path
12105                .commands
12106                .iter()
12107                .filter_map(|command| match command {
12108                    PathCommand::MoveTo(point) | PathCommand::LineTo(point) => {
12109                        Some((point.x - center.x).hypot(point.y - center.y))
12110                    }
12111                    _ => None,
12112                })
12113                .collect();
12114            assert_eq!(radii.len(), 3);
12115            assert!(
12116                radii
12117                    .windows(2)
12118                    .all(|pair| (pair[0] - pair[1]).abs() < 1.0e-9)
12119            );
12120        }
12121        let text: Vec<_> = annotations
12122            .labels
12123            .iter()
12124            .filter_map(|element| match element {
12125                PositionedElement::Text(run) => Some(run.text.as_str()),
12126                _ => None,
12127            })
12128            .collect();
12129        assert!(text.contains(&"North"));
12130        assert!(text.contains(&"West"));
12131    }
12132
12133    #[test]
12134    fn radar_tick_label_positions_are_distinct() {
12135        let radar_plot = format!(
12136            r#"<q:radarChart><q:radarStyle val="marker"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12137            plot_series(0)
12138        );
12139        let chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12140        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12141        let axes = chart.chart.plot_area.axes().unwrap();
12142        let bounds = super::geometry_plot_bounds(chart_bounds()).unwrap();
12143        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12144
12145        let category_axis = axes
12146            .iter()
12147            .find(|axis| axis.kind == AxisKind::Category)
12148            .unwrap();
12149        for (position, expected) in [
12150            (TickLabelPosition::High, Point { x: 108.0, y: 0.0 }),
12151            (TickLabelPosition::Low, Point { x: 108.0, y: 25.0 }),
12152            (TickLabelPosition::NextTo, Point { x: 108.0, y: 5.0 }),
12153        ] {
12154            let mut axis = category_axis.clone();
12155            axis.tick_label_position = position;
12156            let annotations =
12157                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12158                    .unwrap();
12159            let north = annotations
12160                .labels
12161                .iter()
12162                .find_map(|element| match element {
12163                    PositionedElement::Text(run) if run.text == "North" => Some(run),
12164                    _ => None,
12165                })
12166                .unwrap();
12167            assert_point_near(north.origin, expected);
12168        }
12169        let mut hidden_category = category_axis.clone();
12170        hidden_category.tick_label_position = TickLabelPosition::None;
12171        let annotations = super::render_axes_and_labels(
12172            plot,
12173            &[hidden_category],
12174            bounds,
12175            DispBlanksAs::Gap,
12176            &mut fonts,
12177        )
12178        .unwrap();
12179        assert!(annotations.labels.is_empty());
12180
12181        let narrow_bounds = Rect {
12182            x: 36.0,
12183            y: 12.0,
12184            width: 24.0,
12185            height: 100.0,
12186        };
12187        let narrow_center = Point { x: 48.0, y: 62.0 };
12188        assert_eq!(
12189            super::radar_category_label_origin(
12190                narrow_bounds,
12191                narrow_center,
12192                12.0,
12193                1,
12194                4,
12195                TickLabelPosition::High,
12196            ),
12197            Some(Point { x: 72.0, y: 65.0 })
12198        );
12199        assert_eq!(
12200            super::radar_category_label_origin(
12201                narrow_bounds,
12202                narrow_center,
12203                12.0,
12204                1,
12205                4,
12206                TickLabelPosition::NextTo,
12207            ),
12208            Some(Point { x: 66.0, y: 65.0 })
12209        );
12210
12211        let value_axis = axes
12212            .iter()
12213            .find(|axis| axis.kind == AxisKind::Value)
12214            .unwrap();
12215        for (position, expected) in [
12216            (TickLabelPosition::High, Point { x: 166.0, y: 65.0 }),
12217            (TickLabelPosition::Low, Point { x: 32.0, y: 65.0 }),
12218            (TickLabelPosition::NextTo, Point { x: 116.0, y: 65.0 }),
12219        ] {
12220            let mut axis = value_axis.clone();
12221            axis.tick_label_position = position;
12222            let annotations =
12223                super::render_axes_and_labels(plot, &[axis], bounds, DispBlanksAs::Gap, &mut fonts)
12224                    .unwrap();
12225            let zero = annotations
12226                .labels
12227                .iter()
12228                .find_map(|element| match element {
12229                    PositionedElement::Text(run) if run.text == "0" => Some(run),
12230                    _ => None,
12231                })
12232                .unwrap();
12233            assert_point_near(zero.origin, expected);
12234        }
12235        let mut hidden_value = value_axis.clone();
12236        hidden_value.tick_label_position = TickLabelPosition::None;
12237        let annotations = super::render_axes_and_labels(
12238            plot,
12239            &[hidden_value],
12240            bounds,
12241            DispBlanksAs::Gap,
12242            &mut fonts,
12243        )
12244        .unwrap();
12245        assert!(annotations.labels.is_empty());
12246    }
12247
12248    #[test]
12249    fn radar_negative_and_mixed_sign_domains_preserve_values() {
12250        let radar_plot = format!(
12251            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12252            plot_series(0)
12253        );
12254        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12255        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12256        let Plot::Radar { series, .. } = plot else {
12257            panic!("expected radar plot");
12258        };
12259        series[0].values.values = vec![-10.0, -5.0];
12260        let points =
12261            super::radar_series_points(series, chart_bounds(), super::GeometryOptions::default())
12262                .unwrap();
12263        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 70.0 }));
12264        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 105.0 }));
12265
12266        series[0].values.values = vec![-5.0, 10.0];
12267        let options = super::GeometryOptions {
12268            value: Some(super::AxisMapping {
12269                domain: Domain {
12270                    min: -10.0,
12271                    max: 10.0,
12272                },
12273                orientation: Orientation::MinMax,
12274            }),
12275            ..super::GeometryOptions::default()
12276        };
12277        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12278        assert_eq!(points[0][0], (0, Point { x: 100.0, y: 52.5 }));
12279        assert_eq!(points[0][1], (1, Point { x: 100.0, y: 140.0 }));
12280    }
12281
12282    #[test]
12283    fn radar_explicit_bounds_suppress_out_of_range_points_and_labels() {
12284        let radar_plot = format!(
12285            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>"#,
12286            plot_series(0)
12287        );
12288        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12289        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12290        let Plot::Radar { series, .. } = plot else {
12291            panic!("expected radar plot");
12292        };
12293        series[0].values.values = vec![25.0, 75.0];
12294        let options = super::GeometryOptions {
12295            value: Some(super::AxisMapping {
12296                domain: Domain {
12297                    min: 50.0,
12298                    max: 100.0,
12299                },
12300                orientation: Orientation::MinMax,
12301            }),
12302            ..super::GeometryOptions::default()
12303        };
12304        let points = super::radar_series_points(series, chart_bounds(), options).unwrap();
12305        assert_eq!(points[0], vec![(1, Point { x: 100.0, y: 105.0 })]);
12306        let anchors =
12307            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12308        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 1)]);
12309
12310        let Plot::Radar { series, .. } = plot else {
12311            panic!("expected radar plot");
12312        };
12313        series[0].values.values = vec![75.0, 125.0];
12314        let reversed = super::GeometryOptions {
12315            value: Some(super::AxisMapping {
12316                domain: Domain {
12317                    min: 50.0,
12318                    max: 100.0,
12319                },
12320                orientation: Orientation::MaxMin,
12321            }),
12322            ..super::GeometryOptions::default()
12323        };
12324        let points = super::radar_series_points(series, chart_bounds(), reversed).unwrap();
12325        assert_eq!(points[0], vec![(0, Point { x: 100.0, y: 35.0 })]);
12326        let anchors =
12327            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12328        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12329
12330        let value_axis = chart
12331            .chart
12332            .plot_area
12333            .axes
12334            .iter_mut()
12335            .find(|axis| axis.kind == AxisKind::Value)
12336            .unwrap();
12337        value_axis.scaling.minimum = Some(50.0);
12338        value_axis.scaling.maximum = Some(100.0);
12339        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12340        let Plot::Radar { series, .. } = plot else {
12341            panic!("expected radar plot");
12342        };
12343        series[0].values.values = vec![25.0, 25.0];
12344        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12345        let rendered = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12346        assert!(rendered.children.iter().any(|element| matches!(
12347            element,
12348            PositionedElement::Group(group) if group.clip.is_some() && group.children.is_empty()
12349        )));
12350        assert!(
12351            rendered
12352                .children
12353                .iter()
12354                .any(|element| matches!(element, PositionedElement::Path(_)))
12355        );
12356        assert!(!rendered.children.iter().any(|element| matches!(
12357            element,
12358            PositionedElement::Text(run) if run.text == "25"
12359        )));
12360    }
12361
12362    #[test]
12363    fn inside_and_outside_label_positions_follow_family_geometry() {
12364        let mut horizontal =
12365            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12366        let plot = &mut horizontal.chart.plot_area.plots_mut().unwrap()[0];
12367        let Plot::Bar { direction, .. } = plot else {
12368            panic!("expected bar plot");
12369        };
12370        *direction = BarDirection::Bar;
12371        let anchors = super::plot_label_anchors(
12372            plot,
12373            chart_bounds(),
12374            DispBlanksAs::Gap,
12375            super::GeometryOptions::default(),
12376        )
12377        .unwrap();
12378        let anchor = anchors[&(0, 0)];
12379        assert_point_near(
12380            anchor.origin(Some(DataLabelPosition::InsideBase)),
12381            Point { x: 5.0, y: 35.0 },
12382        );
12383        assert_point_near(
12384            anchor.origin(Some(DataLabelPosition::InsideEnd)),
12385            Point { x: 95.0, y: 35.0 },
12386        );
12387        assert_point_near(
12388            anchor.origin(Some(DataLabelPosition::OutsideEnd)),
12389            Point { x: 105.0, y: 35.0 },
12390        );
12391
12392        let mut negative =
12393            CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12394        let plot = &mut negative.chart.plot_area.plots_mut().unwrap()[0];
12395        let Plot::Bar { series, .. } = plot else {
12396            panic!("expected bar plot");
12397        };
12398        series[0].values.values = vec![-1.0, -2.0];
12399        let anchors = super::plot_label_anchors(
12400            plot,
12401            chart_bounds(),
12402            DispBlanksAs::Gap,
12403            super::GeometryOptions::default(),
12404        )
12405        .unwrap();
12406        assert_point_near(
12407            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12408            Point { x: 50.0, y: 75.0 },
12409        );
12410        let reversed = super::GeometryOptions {
12411            value: Some(super::AxisMapping {
12412                domain: Domain {
12413                    min: -2.0,
12414                    max: 0.0,
12415                },
12416                orientation: Orientation::MaxMin,
12417            }),
12418            ..super::GeometryOptions::default()
12419        };
12420        let anchors =
12421            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, reversed).unwrap();
12422        assert_point_near(
12423            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12424            Point { x: 50.0, y: 65.0 },
12425        );
12426
12427        let pie = CT_ChartSpace::from_xml(
12428            chart_with_optional_axes(
12429                &remaining_plot_fixtures()
12430                    .into_iter()
12431                    .find(|(kind, _, _)| *kind == "pieChart")
12432                    .unwrap()
12433                    .1,
12434                false,
12435            )
12436            .as_bytes(),
12437        )
12438        .unwrap();
12439        let Plot::Pie { series, .. } = &pie.chart.plot_area.plots().unwrap()[0] else {
12440            panic!("expected pie plot");
12441        };
12442        let pie_slice = &super::pie_slices(30, None, series, chart_bounds()).unwrap()[0];
12443        assert_point_near(
12444            super::LabelAnchor::segment(pie_slice.anchor, pie_slice.base, pie_slice.end)
12445                .origin(Some(DataLabelPosition::OutsideEnd)),
12446            Point { x: 175.0, y: 70.0 },
12447        );
12448        let doughnut_slice = &super::pie_slices(30, Some(60), series, chart_bounds()).unwrap()[0];
12449        assert_point_near(
12450            super::LabelAnchor::segment(
12451                doughnut_slice.anchor,
12452                doughnut_slice.base,
12453                doughnut_slice.end,
12454            )
12455            .origin(Some(DataLabelPosition::InsideBase)),
12456            Point { x: 147.0, y: 70.0 },
12457        );
12458
12459        let short = super::LabelAnchor::segment(
12460            Point { x: 2.0, y: 0.0 },
12461            Point { x: 0.0, y: 0.0 },
12462            Point { x: 4.0, y: 0.0 },
12463        );
12464        assert_eq!(
12465            short.origin(Some(DataLabelPosition::InsideBase)),
12466            Point { x: 2.0, y: 0.0 }
12467        );
12468        assert_eq!(
12469            short.origin(Some(DataLabelPosition::InsideEnd)),
12470            Point { x: 2.0, y: 0.0 }
12471        );
12472        let thin_bounds = Rect {
12473            x: 0.0,
12474            y: 0.0,
12475            width: 80.0,
12476            height: 80.0,
12477        };
12478        let thin = &super::pie_slices(30, Some(90), series, thin_bounds).unwrap()[0];
12479        let thin = super::LabelAnchor::segment(thin.anchor, thin.base, thin.end);
12480        assert_point_near(
12481            thin.origin(Some(DataLabelPosition::InsideBase)),
12482            Point { x: 78.0, y: 40.0 },
12483        );
12484        assert_point_near(
12485            thin.origin(Some(DataLabelPosition::InsideEnd)),
12486            Point { x: 78.0, y: 40.0 },
12487        );
12488    }
12489
12490    #[test]
12491    fn degenerate_bar_and_radar_anchors_preserve_family_direction() {
12492        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12493        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12494        let Plot::Bar { series, .. } = plot else {
12495            panic!("expected bar plot");
12496        };
12497        series[0].values.values = vec![80.0, 125.0];
12498        for (direction, orientation, boundary, outside) in [
12499            (
12500                BarDirection::Column,
12501                Orientation::MinMax,
12502                Point { x: 50.0, y: 140.0 },
12503                Point { x: 50.0, y: 135.0 },
12504            ),
12505            (
12506                BarDirection::Column,
12507                Orientation::MaxMin,
12508                Point { x: 50.0, y: 0.0 },
12509                Point { x: 50.0, y: 5.0 },
12510            ),
12511            (
12512                BarDirection::Bar,
12513                Orientation::MinMax,
12514                Point { x: 0.0, y: 35.0 },
12515                Point { x: 5.0, y: 35.0 },
12516            ),
12517            (
12518                BarDirection::Bar,
12519                Orientation::MaxMin,
12520                Point { x: 200.0, y: 35.0 },
12521                Point { x: 195.0, y: 35.0 },
12522            ),
12523        ] {
12524            let Plot::Bar {
12525                direction: plot_direction,
12526                ..
12527            } = plot
12528            else {
12529                panic!("expected bar plot");
12530            };
12531            *plot_direction = direction;
12532            let options = super::GeometryOptions {
12533                value: Some(super::AxisMapping {
12534                    domain: Domain {
12535                        min: 80.0,
12536                        max: 100.0,
12537                    },
12538                    orientation,
12539                }),
12540                ..super::GeometryOptions::default()
12541            };
12542            let anchors =
12543                super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options)
12544                    .unwrap();
12545            let anchor = anchors[&(0, 0)];
12546            assert_point_near(anchor.center, boundary);
12547            assert_point_near(anchor.base, boundary);
12548            assert_point_near(anchor.end, boundary);
12549            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideBase)), boundary);
12550            assert_point_near(anchor.origin(Some(DataLabelPosition::InsideEnd)), boundary);
12551            assert_point_near(anchor.origin(Some(DataLabelPosition::OutsideEnd)), outside);
12552        }
12553
12554        let radar_plot = format!(
12555            r#"<q:radarChart><q:radarStyle val="standard"/>{}<q:axId val="-1884094432"/><q:axId val="-1884097184"/></q:radarChart>"#,
12556            plot_series(0)
12557        );
12558        let mut radar = CT_ChartSpace::from_xml(chart_with_plot(&radar_plot).as_bytes()).unwrap();
12559        let plot = &mut radar.chart.plot_area.plots_mut().unwrap()[0];
12560        let Plot::Radar { series, .. } = plot else {
12561            panic!("expected radar plot");
12562        };
12563        series[0].values.values = vec![0.0, 0.0, 0.0, 0.0];
12564        let anchors = super::plot_label_anchors(
12565            plot,
12566            chart_bounds(),
12567            DispBlanksAs::Gap,
12568            super::GeometryOptions::default(),
12569        )
12570        .unwrap();
12571        let center = Point { x: 100.0, y: 70.0 };
12572        for logical_index in 0..4 {
12573            assert_point_near(
12574                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideBase)),
12575                center,
12576            );
12577            assert_point_near(
12578                anchors[&(0, logical_index)].origin(Some(DataLabelPosition::InsideEnd)),
12579                center,
12580            );
12581        }
12582        assert_point_near(
12583            anchors[&(0, 0)].origin(Some(DataLabelPosition::OutsideEnd)),
12584            Point { x: 100.0, y: 65.0 },
12585        );
12586        assert_point_near(
12587            anchors[&(0, 1)].origin(Some(DataLabelPosition::OutsideEnd)),
12588            Point { x: 105.0, y: 70.0 },
12589        );
12590        assert_point_near(
12591            anchors[&(0, 2)].origin(Some(DataLabelPosition::OutsideEnd)),
12592            Point { x: 100.0, y: 75.0 },
12593        );
12594        assert_point_near(
12595            anchors[&(0, 3)].origin(Some(DataLabelPosition::OutsideEnd)),
12596            Point { x: 95.0, y: 70.0 },
12597        );
12598    }
12599
12600    #[test]
12601    fn labels_outside_explicit_axis_bounds_are_suppressed() {
12602        let mut chart =
12603            CT_ChartSpace::from_xml(chart_with_plot(&line_plot("")).as_bytes()).unwrap();
12604        {
12605            let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12606            let Plot::Line { series, .. } = plot else {
12607                panic!("expected line plot");
12608            };
12609            series[0].values.values = vec![25.0, 75.0];
12610        }
12611        let value_axis = chart
12612            .chart
12613            .plot_area
12614            .axes
12615            .iter_mut()
12616            .find(|axis| axis.kind == AxisKind::Value)
12617            .unwrap();
12618        value_axis.scaling.minimum = Some(50.0);
12619        value_axis.scaling.maximum = Some(100.0);
12620        value_axis.scaling.orientation = Orientation::MaxMin;
12621        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12622        let axes = chart.chart.plot_area.axes().unwrap();
12623        let options = super::geometry_options(plot, &axes, DispBlanksAs::Gap).unwrap();
12624        let anchors =
12625            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12626        assert!(!anchors.contains_key(&(0, 0)));
12627        assert_point_near(anchors[&(0, 1)].center, Point { x: 150.0, y: 70.0 });
12628
12629        let mut bars = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12630        let plot = &mut bars.chart.plot_area.plots_mut().unwrap()[0];
12631        let Plot::Bar { series, .. } = plot else {
12632            panic!("expected bar plot");
12633        };
12634        series[0].values.values = vec![90.0, 125.0];
12635        let options = super::GeometryOptions {
12636            value: Some(super::AxisMapping {
12637                domain: Domain {
12638                    min: 80.0,
12639                    max: 100.0,
12640                },
12641                orientation: Orientation::MinMax,
12642            }),
12643            ..super::GeometryOptions::default()
12644        };
12645        let anchors =
12646            super::plot_label_anchors(plot, chart_bounds(), DispBlanksAs::Gap, options).unwrap();
12647        assert_eq!(anchors.keys().copied().collect::<Vec<_>>(), vec![(0, 0)]);
12648        assert_point_near(anchors[&(0, 0)].base, Point { x: 50.0, y: 140.0 });
12649        assert_point_near(anchors[&(0, 0)].end, Point { x: 50.0, y: 70.0 });
12650        assert_point_near(anchors[&(0, 0)].center, Point { x: 50.0, y: 105.0 });
12651        assert_point_near(
12652            anchors[&(0, 0)].origin(Some(DataLabelPosition::InsideBase)),
12653            Point { x: 50.0, y: 135.0 },
12654        );
12655    }
12656
12657    #[test]
12658    fn zero_bars_render_and_emit_requested_value_labels() {
12659        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12660        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
12661        let Plot::Bar { series, .. } = plot else {
12662            panic!("expected bar plot");
12663        };
12664        series[0].values.values = vec![0.0, 0.0];
12665        let geometry = render_geometry(&chart.chart, chart_bounds()).unwrap();
12666        assert_eq!(path_commands(&geometry).len(), 2);
12667        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12668        render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12669        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12670        let mut labels = Vec::new();
12671        super::render_data_labels(
12672            plot,
12673            chart_bounds(),
12674            DispBlanksAs::Gap,
12675            super::GeometryOptions::default(),
12676            &mut fonts,
12677            &mut labels,
12678        )
12679        .unwrap();
12680        let text: Vec<_> = labels
12681            .iter()
12682            .filter_map(|element| match element {
12683                PositionedElement::Text(run) => Some(run.text.as_str()),
12684                _ => None,
12685            })
12686            .collect();
12687        assert_eq!(text, vec!["0", "0"]);
12688    }
12689
12690    #[test]
12691    fn axes_gridlines_and_tick_marks_follow_model_state() {
12692        let chart = labelled_bar_chart();
12693        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12694        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12695        let ranks: Vec<_> = group
12696            .children
12697            .iter()
12698            .map(|element| match element {
12699                PositionedElement::Group(group) => {
12700                    assert!(group.clip.is_some());
12701                    1
12702                }
12703                PositionedElement::Path(path)
12704                    if path
12705                        .stroke
12706                        .as_ref()
12707                        .is_some_and(|stroke| stroke.width == 0.5) =>
12708                {
12709                    0
12710                }
12711                PositionedElement::Path(path) if path.fill.is_some() => 4,
12712                PositionedElement::Path(path) => {
12713                    let bounds = path.path.bounds().expect("axis or tick bounds");
12714                    assert_eq!(path.stroke.as_ref().map(|stroke| stroke.width), Some(1.0));
12715                    if bounds.width > 4.0 || bounds.height > 4.0 {
12716                        2
12717                    } else {
12718                        3
12719                    }
12720                }
12721                PositionedElement::Text(_) => 5,
12722                _ => panic!("unexpected labelled chart element"),
12723            })
12724            .collect();
12725        assert!(ranks.windows(2).all(|pair| pair[0] <= pair[1]), "{ranks:?}");
12726        for required in 0..=5 {
12727            assert!(
12728                ranks.contains(&required),
12729                "missing z-order rank {required}: {ranks:?}"
12730            );
12731        }
12732        assert_eq!(ranks.iter().filter(|rank| **rank == 1).count(), 1);
12733        assert_eq!(ranks.iter().filter(|rank| **rank == 2).count(), 2);
12734
12735        let mut deleted = chart.clone();
12736        for axis in &mut deleted.chart.plot_area.axes {
12737            axis.deleted = true;
12738        }
12739        let deleted_group = render_chart(&deleted.chart, chart_bounds(), &mut fonts).unwrap();
12740        assert_eq!(
12741            deleted_group
12742                .children
12743                .iter()
12744                .filter(|element| {
12745                    matches!(
12746                        element,
12747                        PositionedElement::Path(path) if path.stroke.is_some()
12748                    )
12749                })
12750                .count(),
12751            0
12752        );
12753    }
12754
12755    #[test]
12756    fn category_ticks_cover_unlabelled_and_sparse_logical_slots() {
12757        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12758        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12759            panic!("expected bar plot");
12760        };
12761        series[0].categories = None;
12762        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12763        let mut category_axis = chart
12764            .chart
12765            .plot_area
12766            .axes()
12767            .unwrap()
12768            .into_iter()
12769            .find(|axis| axis.kind == AxisKind::Category)
12770            .unwrap();
12771        category_axis.major_gridlines = Some(super::ChartLines::default());
12772        category_axis.major_tick_mark = TickMark::Cross;
12773        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12774        let annotations = super::render_axes_and_labels(
12775            plot,
12776            &[category_axis],
12777            chart_bounds(),
12778            DispBlanksAs::Gap,
12779            &mut fonts,
12780        )
12781        .unwrap();
12782        assert_eq!(annotations.gridlines.len(), 2);
12783        assert_eq!(annotations.ticks.len(), 2);
12784        assert!(annotations.labels.is_empty());
12785
12786        let sparse_plot = format!(
12787            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>"#,
12788            sparse_category_series()
12789        );
12790        let sparse = CT_ChartSpace::from_xml(chart_with_plot(&sparse_plot).as_bytes()).unwrap();
12791        let plot = &sparse.chart.plot_area.plots().unwrap()[0];
12792        let mut category_axis = sparse
12793            .chart
12794            .plot_area
12795            .axes()
12796            .unwrap()
12797            .into_iter()
12798            .find(|axis| axis.kind == AxisKind::Category)
12799            .unwrap();
12800        category_axis.major_gridlines = Some(super::ChartLines::default());
12801        category_axis.major_tick_mark = TickMark::Cross;
12802        let annotations = super::render_axes_and_labels(
12803            plot,
12804            &[category_axis],
12805            chart_bounds(),
12806            DispBlanksAs::Gap,
12807            &mut fonts,
12808        )
12809        .unwrap();
12810        assert_eq!(annotations.gridlines.len(), 3);
12811        assert_eq!(annotations.ticks.len(), 3);
12812        assert_eq!(annotations.labels.len(), 2);
12813
12814        let oversized_plot = sparse_plot.replacen(
12815            r#"<q:ptCount val="3"/>"#,
12816            r#"<q:ptCount val="4294967295"/>"#,
12817            1,
12818        );
12819        let oversized =
12820            CT_ChartSpace::from_xml(chart_with_plot(&oversized_plot).as_bytes()).unwrap();
12821        let plot = &oversized.chart.plot_area.plots().unwrap()[0];
12822        let category_axis = oversized
12823            .chart
12824            .plot_area
12825            .axes()
12826            .unwrap()
12827            .into_iter()
12828            .find(|axis| axis.kind == AxisKind::Category)
12829            .unwrap();
12830        let result = super::render_axes_and_labels(
12831            plot,
12832            &[category_axis],
12833            chart_bounds(),
12834            DispBlanksAs::Gap,
12835            &mut fonts,
12836        );
12837        let error = match result {
12838            Err(error) => error,
12839            Ok(_) => panic!("oversized category annotations rendered"),
12840        };
12841        assert!(
12842            error
12843                .to_string()
12844                .contains("logical category count 4294967295")
12845        );
12846        assert!(error.to_string().contains("exceeds 16384"));
12847    }
12848
12849    #[test]
12850    fn unsupported_numeric_category_formats_return_projection_errors() {
12851        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
12852        let Plot::Bar { series, .. } = &mut chart.chart.plot_area.plots_mut().unwrap()[0] else {
12853            panic!("expected bar plot");
12854        };
12855        series[0].categories = Some(AxisData::Numeric(
12856            NumericData::new(
12857                "Sheet1!$A$2:$A$3".to_owned(),
12858                "General".to_owned(),
12859                vec![1.0, 2.0],
12860            )
12861            .unwrap(),
12862        ));
12863        let plot = &chart.chart.plot_area.plots().unwrap()[0];
12864        let mut category_axis = chart
12865            .chart
12866            .plot_area
12867            .axes()
12868            .unwrap()
12869            .into_iter()
12870            .find(|axis| axis.kind == AxisKind::Category)
12871            .unwrap();
12872        category_axis.number_format = Some(NumberFormat::new("0.00".to_owned(), false).unwrap());
12873        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12874        let annotations = super::render_axes_and_labels(
12875            plot,
12876            &[category_axis.clone()],
12877            chart_bounds(),
12878            DispBlanksAs::Gap,
12879            &mut fonts,
12880        )
12881        .unwrap();
12882        let text: Vec<_> = annotations
12883            .labels
12884            .iter()
12885            .filter_map(|element| match element {
12886                PositionedElement::Text(run) => Some(run.text.as_str()),
12887                _ => None,
12888            })
12889            .collect();
12890        assert_eq!(text, vec!["1.00", "2.00"]);
12891
12892        category_axis.number_format = Some(NumberFormat::new("#,##0".to_owned(), false).unwrap());
12893        let result = super::render_axes_and_labels(
12894            plot,
12895            &[category_axis],
12896            chart_bounds(),
12897            DispBlanksAs::Gap,
12898            &mut fonts,
12899        );
12900        let error = match result {
12901            Err(error) => error,
12902            Ok(_) => panic!("unsupported category-axis format rendered"),
12903        };
12904        assert!(
12905            error
12906                .to_string()
12907                .contains("c:numFmt/@formatCode projection")
12908        );
12909        assert!(error.to_string().contains("#,##0"));
12910    }
12911
12912    #[test]
12913    fn labels_and_legend_shape_with_deterministic_fonts() {
12914        let chart = labelled_bar_chart();
12915        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12916        let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12917        let labels: Vec<_> = group
12918            .children
12919            .iter()
12920            .filter_map(|element| match element {
12921                PositionedElement::Text(run) => Some(run),
12922                _ => None,
12923            })
12924            .collect();
12925        for text in ["North", "South", "20", "Sales", "100"] {
12926            assert!(labels.iter().any(|run| run.text == text), "missing {text}");
12927        }
12928        let styled_tick = labels.iter().find(|run| run.text == "20").unwrap();
12929        assert_eq!(styled_tick.font_size, 12.0);
12930        assert!(styled_tick.bold);
12931        assert!(
12932            labels
12933                .iter()
12934                .all(|run| !run.glyph_ids.is_empty() && run.glyph_ids.len() == run.advances.len())
12935        );
12936    }
12937
12938    #[test]
12939    fn labelled_chart_raster_is_deterministic() {
12940        let raster = || {
12941            let chart = labelled_bar_chart();
12942            let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
12943            let group = render_chart(&chart.chart, chart_bounds(), &mut fonts).unwrap();
12944            let layout = LayoutResult::new(
12945                vec![PageFrame::new(
12946                    1,
12947                    200.0,
12948                    140.0,
12949                    vec![PositionedElement::Group(group)],
12950                )],
12951                fonts.all_font_data(),
12952                None,
12953                Vec::new(),
12954            );
12955            oxml_pdf::render_page_to_png(&layout, 0, 72.0).unwrap()
12956        };
12957        assert_eq!(raster(), raster());
12958    }
12959
12960    fn labelled_bar_chart() -> CT_ChartSpace {
12961        let series = plot_series(0)
12962            .replace(
12963                "<q:cat>",
12964                "<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>",
12965            )
12966            .replace("<q:v>1</q:v>", "<q:v>0</q:v>")
12967            .replace("<q:v>2</q:v>", "<q:v>100</q:v>");
12968        let plot = format!(
12969            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>"#
12970        );
12971        let xml = chart_with_plot(&plot)
12972            .replacen(
12973                "<q:scaling/>",
12974                "<q:scaling><q:orientation val=\"minMax\"/></q:scaling>",
12975                1,
12976            )
12977            .replacen(
12978                "<q:scaling/>",
12979                "<q:scaling><q:max val=\"100\"/><q:min val=\"0\"/></q:scaling>",
12980                1,
12981            )
12982            .replace(
12983                "<q:axPos val=\"b\"/>",
12984                "<q:axPos val=\"b\"/><q:majorTickMark val=\"cross\"/>",
12985            )
12986            .replace(
12987                "<q:axPos val=\"l\"/>",
12988                "<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>",
12989            )
12990            .replace("</q:plotArea></q:chart>", "</q:plotArea><q:legend/></q:chart>");
12991        CT_ChartSpace::from_xml(xml.as_bytes()).unwrap()
12992    }
12993
12994    fn chart_bounds() -> Rect {
12995        Rect {
12996            x: 0.0,
12997            y: 0.0,
12998            width: 200.0,
12999            height: 140.0,
13000        }
13001    }
13002
13003    fn four_series_bar_chart() -> CT_ChartSpace {
13004        let series = (0..4).map(plot_series).collect::<String>();
13005        let plot = bar_plot("").replace(&plot_series(0), &series);
13006        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13007    }
13008
13009    fn seven_series_bar_chart() -> CT_ChartSpace {
13010        let series = (0..7).map(plot_series).collect::<String>();
13011        let plot = bar_plot("").replace(&plot_series(0), &series);
13012        CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap()
13013    }
13014
13015    fn path_colours(geometry: &ChartGeometry) -> Vec<Color> {
13016        let mut colours = geometry_children(geometry)
13017            .iter()
13018            .filter_map(|element| match element {
13019                PositionedElement::Path(path) => match path
13020                    .fill
13021                    .as_ref()
13022                    .or_else(|| path.stroke.as_ref().map(|stroke| &stroke.paint))
13023                {
13024                    Some(Paint::Solid(colour)) => Some(*colour),
13025                    _ => None,
13026                },
13027                _ => None,
13028            })
13029            .collect::<Vec<_>>();
13030        colours.dedup_by(|left, right| left == right);
13031        colours
13032    }
13033
13034    fn resolved_layout_colour(colour: oxml_drawing::color::ResolvedColor) -> Color {
13035        Color {
13036            r: f64::from(colour.red) / 255.0,
13037            g: f64::from(colour.green) / 255.0,
13038            b: f64::from(colour.blue) / 255.0,
13039            a: f64::from(colour.alpha) / 255.0,
13040        }
13041    }
13042
13043    fn geometry_children(geometry: &ChartGeometry) -> &[PositionedElement] {
13044        let [PositionedElement::Group(group)] = geometry.elements.as_slice() else {
13045            panic!("chart geometry must contain one group");
13046        };
13047        &group.children
13048    }
13049
13050    fn path_commands(geometry: &ChartGeometry) -> Vec<&[PathCommand]> {
13051        geometry_children(geometry)
13052            .iter()
13053            .map(|element| {
13054                let PositionedElement::Path(path) = element else {
13055                    panic!("chart geometry child must be a path");
13056                };
13057                path.path.commands.as_slice()
13058            })
13059            .collect()
13060    }
13061
13062    fn path_bounds(geometry: &ChartGeometry) -> Vec<Rect> {
13063        geometry_children(geometry)
13064            .iter()
13065            .map(|element| {
13066                let PositionedElement::Path(path) = element else {
13067                    panic!("chart geometry child must be a path");
13068                };
13069                path.path.bounds().expect("chart path has bounds")
13070            })
13071            .collect()
13072    }
13073
13074    fn assert_rect_near(actual: Rect, expected: Rect) {
13075        assert_near(actual.x, expected.x);
13076        assert_near(actual.y, expected.y);
13077        assert_near(actual.width, expected.width);
13078        assert_near(actual.height, expected.height);
13079    }
13080
13081    fn assert_rects_near(actual: &[Rect], expected: &[Rect]) {
13082        assert_eq!(actual.len(), expected.len());
13083        for (actual, expected) in actual.iter().copied().zip(expected.iter().copied()) {
13084            assert_rect_near(actual, expected);
13085        }
13086    }
13087
13088    fn assert_path_points(commands: &[PathCommand], expected: &[Point], closed: bool) {
13089        let points: Vec<_> = commands
13090            .iter()
13091            .filter_map(|command| match command {
13092                PathCommand::MoveTo(point) | PathCommand::LineTo(point) => Some(*point),
13093                PathCommand::Close => None,
13094                PathCommand::CurveTo { .. } => panic!("expected polygonal path"),
13095            })
13096            .collect();
13097        assert_eq!(points.len(), expected.len());
13098        for (actual, expected) in points.into_iter().zip(expected.iter().copied()) {
13099            assert_point_near(actual, expected);
13100        }
13101        assert_eq!(commands.last() == Some(&PathCommand::Close), closed);
13102    }
13103
13104    fn assert_point_near(actual: Point, expected: Point) {
13105        assert_near(actual.x, expected.x);
13106        assert_near(actual.y, expected.y);
13107    }
13108
13109    fn assert_near(actual: f64, expected: f64) {
13110        assert!(
13111            (actual - expected).abs() < 1e-9,
13112            "expected {expected}, got {actual}"
13113        );
13114    }
13115
13116    #[test]
13117    fn remaining_v1_plots_round_trip_and_render() {
13118        for (kind, plot, axes) in remaining_plot_fixtures() {
13119            let xml = chart_with_optional_axes(&plot, axes);
13120            let parsed = CT_ChartSpace::from_xml(xml.as_bytes())
13121                .unwrap_or_else(|error| panic!("{kind}: parse failed: {error}"));
13122            assert_eq!(parsed.chart.plot_area.plots().unwrap().len(), 1);
13123            assert_eq!(
13124                parsed.chart.plot_area.axes().unwrap().len(),
13125                usize::from(axes) * 2
13126            );
13127            let written = parsed
13128                .to_xml()
13129                .unwrap_or_else(|error| panic!("{kind}: write failed: {error}"));
13130            assert_eq!(
13131                parsed,
13132                CT_ChartSpace::from_xml(&written)
13133                    .unwrap_or_else(|error| panic!("{kind}: reparse failed: {error}"))
13134            );
13135        }
13136        if let Some(corpus) = require_or_skip_corpus() {
13137            verify_remaining_plot_viewer_gate(&corpus);
13138        }
13139    }
13140
13141    #[test]
13142    fn remaining_plot_families_write_fixed_prefixes_in_schema_order() {
13143        for (kind, plot, axes) in remaining_plot_fixtures() {
13144            let parsed =
13145                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13146            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13147            assert!(written.contains(&format!("<c:{kind}")));
13148            assert!(!written.contains(&format!("<q:{kind}")));
13149            let series = written.find("<c:ser").unwrap();
13150            let labels = written.find("<c:dLbls").unwrap();
13151            assert!(series < labels, "{kind}: series must precede labels");
13152            if matches!(kind, "areaChart" | "scatterChart" | "radarChart") {
13153                let property = match kind {
13154                    "areaChart" => "<c:grouping",
13155                    "scatterChart" => "<c:scatterStyle",
13156                    "radarChart" => "<c:radarStyle",
13157                    _ => unreachable!(),
13158                };
13159                assert!(written.find(property).unwrap() < series);
13160                assert!(labels < written.find("<c:axId").unwrap());
13161            } else {
13162                assert!(labels < written.find("<c:firstSliceAng").unwrap());
13163            }
13164            if kind == "doughnutChart" {
13165                assert!(
13166                    written.find("<c:firstSliceAng").unwrap()
13167                        < written.find("<c:holeSize").unwrap()
13168                );
13169            }
13170        }
13171    }
13172
13173    #[test]
13174    fn scatter_series_map_numeric_categories_and_values_to_x_and_y() {
13175        let plot = remaining_plot_fixtures()
13176            .into_iter()
13177            .find(|(kind, _, _)| *kind == "scatterChart")
13178            .unwrap()
13179            .1;
13180        let mut parsed =
13181            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, true).as_bytes()).unwrap();
13182        let standalone_series = {
13183            let Plot::Scatter { series, .. } = &mut parsed.chart.plot_area.plots_mut().unwrap()[0]
13184            else {
13185                panic!("expected scatter plot");
13186            };
13187            let Some(AxisData::Numeric(x_values)) = &mut series[0].categories else {
13188                panic!("expected numeric x values");
13189            };
13190            x_values.values[0] = 3.5;
13191            series[0].values.values[0] = 7.5;
13192            series[0].clone()
13193        };
13194        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13195        assert!(written.contains("<c:xVal>"));
13196        assert!(written.contains("<c:yVal>"));
13197        assert!(!written.contains("<c:cat>"));
13198        assert!(!written.contains("<c:val>"));
13199        assert!(written.contains("<c:v>3.5</c:v>"));
13200        assert!(written.contains("<c:v>7.5</c:v>"));
13201        let standalone = String::from_utf8(standalone_series.to_xml().unwrap()).unwrap();
13202        assert!(standalone.contains("<c:xVal>"));
13203        assert!(standalone.contains("<c:yVal>"));
13204        assert!(!standalone.contains("<c:cat>"));
13205        assert!(!standalone.contains("<c:val>"));
13206    }
13207
13208    #[test]
13209    fn malformed_remaining_plots_return_errors_without_panicking() {
13210        let series = plot_series(0);
13211        let scatter = numeric_plot_series(0);
13212        let opaque_bubble = series.replace(
13213            "</q:val></q:ser>",
13214            "</q:val><q:bubbleSize><x:opaque/></q:bubbleSize></q:ser>",
13215        );
13216        let cases = [
13217            r#"<q:pieChart/>"#.to_owned(),
13218            format!(r#"<q:pieChart>{series}<q:firstSliceAng val="361"/></q:pieChart>"#),
13219            format!(
13220                r#"<q:pieChart>{series}<q:firstSliceAng val="1"/><q:firstSliceAng val="2"/></q:pieChart>"#
13221            ),
13222            format!(r#"<q:doughnutChart>{series}<q:holeSize val="9"/></q:doughnutChart>"#),
13223            format!(
13224                r#"<q:doughnutChart>{series}<q:holeSize val="50"/><q:holeSize val="60"/></q:doughnutChart>"#
13225            ),
13226            format!(
13227                r#"<q:areaChart><q:grouping val="clustered"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13228            ),
13229            format!(
13230                r#"<q:areaChart><q:grouping val="standard"/><q:grouping val="stacked"/>{series}<q:axId val="1"/><q:axId val="2"/></q:areaChart>"#
13231            ),
13232            format!(
13233                r#"<q:scatterChart><q:scatterStyle val="dots"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13234            ),
13235            format!(
13236                r#"<q:scatterChart><q:scatterStyle val="marker"/><q:scatterStyle val="line"/>{scatter}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13237            ),
13238            format!(
13239                r#"<q:radarChart><q:radarStyle val="smooth"/>{series}<q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13240            ),
13241            format!(
13242                r#"<q:radarChart><q:radarStyle val="marker"/>{series}<q:dLbls/><q:dLbls/><q:axId val="1"/><q:axId val="2"/></q:radarChart>"#
13243            ),
13244            format!(r#"<q:pieChart>{series}<q:axId val="1"/></q:pieChart>"#),
13245            format!(
13246                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/></q:areaChart>"#
13247            ),
13248            r#"<q:areaChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:areaChart>"#.to_owned(),
13249            format!(
13250                r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="1"/><q:axId val="2"/><q:axId val="3"/></q:areaChart>"#
13251            ),
13252            format!(
13253                r#"<q:scatterChart><q:scatterStyle val="marker"/>{series}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#
13254            ),
13255            format!(
13256                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13257                scatter
13258                    .replace("<q:yVal>", "<q:val>")
13259                    .replace("</q:yVal>", "</q:val>")
13260            ),
13261            format!(
13262                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13263                scatter_without_x(0)
13264            ),
13265            format!(
13266                r#"<q:scatterChart><q:scatterStyle val="marker"/>{}<q:axId val="1"/><q:axId val="2"/></q:scatterChart>"#,
13267                scatter_without_y(0)
13268            ),
13269            format!(r#"<q:pieChart>{opaque_bubble}</q:pieChart>"#),
13270        ];
13271        for plot in cases {
13272            let xml = chart_with_optional_axes(&plot, false);
13273            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
13274            assert!(result.is_ok(), "parser panicked for {plot}");
13275            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
13276        }
13277
13278        let missing_axis = format!(
13279            r#"<q:areaChart><q:grouping val="standard"/>{series}<q:axId val="-1884094432"/><q:axId val="7"/></q:areaChart>"#
13280        );
13281        let missing_axis =
13282            CT_ChartSpace::from_xml(chart_with_optional_axes(&missing_axis, true).as_bytes())
13283                .unwrap_err();
13284        assert!(missing_axis.to_string().contains("missing axis 7"));
13285
13286        let values =
13287            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
13288        let mut bubble_series = Series::new(0, 0, values.clone());
13289        bubble_series.bubble_size = Some(values);
13290        assert!(Plot::pie(vec![bubble_series]).is_err());
13291    }
13292
13293    #[test]
13294    fn unsupported_plot_families_and_children_remain_byte_preserved() {
13295        for raw in [
13296            r#"<q:pie3DChart x:keep="3d"><q:ser><x:opaque/></q:ser></q:pie3DChart>"#,
13297            r#"<q:ofPieChart x:keep="of"><x:opaque/></q:ofPieChart>"#,
13298            r#"<q:stockChart x:keep="stock"><x:opaque/></q:stockChart>"#,
13299            r#"<q:surfaceChart x:keep="surface"><x:opaque/></q:surfaceChart>"#,
13300            r#"<q:bubbleChart x:keep="bubble"><x:opaque/></q:bubbleChart>"#,
13301        ] {
13302            let parsed =
13303                CT_ChartSpace::from_xml(chart_with_optional_axes(raw, false).as_bytes()).unwrap();
13304            assert!(parsed.chart.plot_area.plots().is_err());
13305            let written = parsed.to_xml().unwrap();
13306            assert!(
13307                written
13308                    .windows(raw.len())
13309                    .any(|window| window == raw.as_bytes())
13310            );
13311        }
13312
13313        let pie = remaining_plot_fixtures()[0].1.clone();
13314        let preserved = r#"<!--point--><q:dPt x:id="1"><q:explosion val="7"/><x:tail/></q:dPt>"#;
13315        let plot = pie.replace("<q:dLbls>", &format!("{preserved}<q:dLbls>"));
13316        let parsed =
13317            CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, false).as_bytes()).unwrap();
13318        let written = parsed.to_xml().unwrap();
13319        assert!(
13320            written
13321                .windows(preserved.len())
13322                .any(|window| window == preserved.as_bytes())
13323        );
13324
13325        for (kind, mut plot, axes) in [
13326            ("pieChart", remaining_plot_fixtures()[0].1.clone(), false),
13327            ("areaChart", remaining_plot_fixtures()[2].1.clone(), true),
13328        ] {
13329            plot = plot.replace(r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#, "");
13330            if kind == "pieChart" {
13331                plot = plot.replace(
13332                    "</q:extLst></q:pieChart>",
13333                    "</q:extLst><!--after-ext--></q:pieChart>",
13334                );
13335            } else {
13336                plot = plot.replace(
13337                    "</q:dropLines><q:axId",
13338                    "</q:dropLines><!--after-drop-lines--><q:axId",
13339                );
13340            }
13341            let mut parsed =
13342                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes()).unwrap();
13343            let labels =
13344                CT_DLbls::from_xml(format!(r#"<c:dLbls xmlns:c="{C_NS}"/>"#).as_bytes()).unwrap();
13345            match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
13346                Plot::Pie { data_labels, .. } | Plot::Area { data_labels, .. } => {
13347                    *data_labels = Some(labels)
13348                }
13349                _ => panic!("expected pie or area"),
13350            }
13351            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13352            assert!(
13353                written.find("<c:dLbls").unwrap() < written.find("<q:extLst").unwrap(),
13354                "{kind}: inserted labels must precede the preserved extension"
13355            );
13356            if kind == "areaChart" {
13357                assert!(written.find("<c:dLbls").unwrap() < written.find("<q:dropLines").unwrap());
13358                assert!(
13359                    written.find("<q:dropLines").unwrap()
13360                        < written.find("<!--after-drop-lines-->").unwrap()
13361                );
13362            } else {
13363                assert!(
13364                    written.find("<q:extLst").unwrap() < written.find("<!--after-ext-->").unwrap()
13365                );
13366            }
13367        }
13368    }
13369
13370    #[test]
13371    fn every_supported_corpus_plot_round_trips_structurally() {
13372        let Some(corpus) = require_or_skip_corpus() else {
13373            return;
13374        };
13375        verify_fetched_corpus(&corpus);
13376        let mut pie_count = 0usize;
13377        let mut bar_count = 0usize;
13378        let mut line_count = 0usize;
13379        for path in manifest_paths() {
13380            let package = OpcPackage::open(corpus.join(path)).unwrap();
13381            for (part, xml) in &package.parts {
13382                if !is_chart_part(part) {
13383                    continue;
13384                }
13385                let parsed = CT_ChartSpace::from_xml(xml).unwrap();
13386                if let Ok(plots) = parsed.chart.plot_area.plots() {
13387                    for plot in plots {
13388                        match plot {
13389                            Plot::Pie { .. } => pie_count += 1,
13390                            Plot::Bar { .. } => bar_count += 1,
13391                            Plot::Line { .. } => line_count += 1,
13392                            _ => {}
13393                        }
13394                    }
13395                }
13396                let written = parsed.to_xml().unwrap();
13397                assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
13398            }
13399        }
13400        assert_eq!(pie_count, 1, "typed corpus pie coverage changed");
13401        assert_eq!(bar_count, 11, "typed corpus bar coverage changed");
13402        assert_eq!(line_count, 2, "typed corpus line coverage changed");
13403    }
13404
13405    fn standalone_axis(local: &str, children: &str) -> String {
13406        format!(
13407            r#"<c:{local} xmlns:c="{C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">{children}</c:{local}>"#
13408        )
13409    }
13410
13411    fn minimal_axis(local: &str, id: &str, position: &str, cross_axis: &str) -> String {
13412        format!(
13413            r#"<c:{local}><c:axId val="{id}"/><c:scaling/><c:axPos val="{position}"/><c:crossAx val="{cross_axis}"/></c:{local}>"#
13414        )
13415    }
13416
13417    fn chart_with_axes(axes: &[String]) -> String {
13418        format!(
13419            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>"#,
13420            axes.join("")
13421        )
13422    }
13423
13424    #[test]
13425    fn data_labels_write_fixed_prefixes_in_schema_order() {
13426        let xml = format!(
13427            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>"#
13428        );
13429        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13430        assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0%");
13431        assert_eq!(parsed.position, Some(DataLabelPosition::OutsideEnd));
13432        assert_eq!(parsed.separator.as_deref(), Some(" / "));
13433        assert!(parsed.show_legend_key);
13434        assert!(parsed.show_value);
13435        assert!(!parsed.show_category_name);
13436        assert!(parsed.show_series_name);
13437        assert!(!parsed.show_percent);
13438        assert!(parsed.show_bubble_size);
13439
13440        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13441        assert!(written.starts_with(&format!(
13442            "<c:dLbls xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13443        )));
13444        let mut cursor = 0usize;
13445        for tag in [
13446            "<q:dLbl",
13447            "<c:numFmt",
13448            "<q:spPr",
13449            "<q:txPr",
13450            "<c:dLblPos",
13451            "<c:showLegendKey",
13452            "<c:showVal",
13453            "<c:showCatName",
13454            "<c:showSerName",
13455            "<c:showPercent",
13456            "<c:showBubbleSize",
13457            "<c:separator",
13458            "<q:leaderLines",
13459            "<q:extLst",
13460        ] {
13461            let position = written[cursor..]
13462                .find(tag)
13463                .unwrap_or_else(|| panic!("missing ordered data-label tag {tag}"));
13464            cursor += position + tag.len();
13465        }
13466        assert_eq!(parsed, CT_DLbls::from_xml(written.as_bytes()).unwrap());
13467    }
13468
13469    #[test]
13470    fn percentage_formatted_label_renders_with_correct_text() {
13471        let Some(corpus) = require_or_skip_corpus() else {
13472            return;
13473        };
13474        verify_fetched_corpus(&corpus);
13475        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
13476        assert_command_version("pdftotext", &["-v"], PDFTOTEXT_VERSION);
13477
13478        let source = corpus.join("bar-chart.pptx");
13479        let mut package = OpcPackage::open(&source).expect("open percentage-label source deck");
13480        let chart_part = "/ppt/charts/chart1.xml";
13481        let original_parts = package.parts.clone();
13482        let chart_xml = package
13483            .get_part(chart_part)
13484            .expect("bar chart part")
13485            .to_vec();
13486        let original_series = first_series_xml(&chart_xml).expect("bar chart series");
13487        let mut series = Series::from_xml(&original_series).expect("parse bar chart series");
13488        series.values.values.fill(0.25);
13489        let labels = CT_DLbls {
13490            number_format: Some(NumberFormat::new("0%".to_owned(), false).unwrap()),
13491            position: Some(DataLabelPosition::OutsideEnd),
13492            show_value: true,
13493            ..CT_DLbls::default()
13494        };
13495        series.data_labels = Some(labels);
13496        let replacement = series.to_xml().expect("serialize typed percentage labels");
13497        let candidate_chart = replace_once(&chart_xml, &original_series, &replacement);
13498        package.set_part(chart_part, candidate_chart);
13499        for (part, bytes) in &original_parts {
13500            if part == chart_part {
13501                assert_ne!(package.parts[part], *bytes);
13502            } else {
13503                assert_eq!(
13504                    package.parts[part], *bytes,
13505                    "unexpected changed part {part}"
13506                );
13507            }
13508        }
13509
13510        let temp_root = std::env::temp_dir().join(format!(
13511            "rpptx-chart-f123-label-gate-{}",
13512            std::process::id()
13513        ));
13514        let profile = std::env::temp_dir().join(format!(
13515            "rpptx-chart-f123-label-profile-{}",
13516            std::process::id()
13517        ));
13518        if temp_root.exists() {
13519            fs::remove_dir_all(&temp_root).expect("remove stale F-123 evidence");
13520        }
13521        if profile.exists() {
13522            fs::remove_dir_all(&profile).expect("remove stale F-123 profile");
13523        }
13524        fs::create_dir_all(&temp_root).expect("create F-123 evidence root");
13525        let unbound_candidate = temp_root.join("f123-percentage-label.pptx");
13526        package
13527            .save(&unbound_candidate)
13528            .expect("save percentage-label candidate");
13529        let candidate_sha = sha256(&unbound_candidate);
13530        let evidence_dir = temp_root.join(&candidate_sha);
13531        fs::create_dir(&evidence_dir).expect("create SHA-bound evidence directory");
13532        let candidate = evidence_dir.join("f123-percentage-label.pptx");
13533        fs::rename(&unbound_candidate, &candidate).expect("bind candidate to evidence SHA");
13534        assert_eq!(sha256(&candidate), candidate_sha);
13535
13536        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
13537        let conversion = Command::new("soffice")
13538            .args([
13539                "--headless",
13540                &profile_argument,
13541                "--convert-to",
13542                "pdf:impress_pdf_Export",
13543                "--outdir",
13544            ])
13545            .arg(&evidence_dir)
13546            .arg(&candidate)
13547            .output()
13548            .expect("run pinned LibreOffice percentage-label gate");
13549        assert!(
13550            conversion.status.success(),
13551            "LibreOffice conversion failed: {}",
13552            String::from_utf8_lossy(&conversion.stderr)
13553        );
13554        let pdf = evidence_dir.join("f123-percentage-label.pdf");
13555        assert!(
13556            pdf.is_file(),
13557            "LibreOffice did not create {}",
13558            pdf.display()
13559        );
13560        let text_path = evidence_dir.join("f123-percentage-label.txt");
13561        let extraction = Command::new("pdftotext")
13562            .arg(&pdf)
13563            .arg(&text_path)
13564            .output()
13565            .expect("run pinned Poppler percentage-label extraction");
13566        assert!(
13567            extraction.status.success(),
13568            "pdftotext failed: {}",
13569            String::from_utf8_lossy(&extraction.stderr)
13570        );
13571        let extracted = fs::read_to_string(&text_path).expect("read percentage-label text");
13572        assert!(
13573            extracted.contains("25%"),
13574            "SHA-bound viewer text lacks 25%: {extracted:?}"
13575        );
13576        for artifact in [&candidate, &pdf, &text_path] {
13577            assert_eq!(
13578                artifact.parent(),
13579                Some(evidence_dir.as_path()),
13580                "evidence escaped candidate SHA"
13581            );
13582        }
13583        eprintln!("F-123 candidate/render/text evidence SHA-256 {candidate_sha}, extracted 25%");
13584        fs::remove_dir_all(&temp_root).expect("remove F-123 evidence");
13585        if profile.exists() {
13586            fs::remove_dir_all(&profile).expect("remove F-123 profile");
13587        }
13588    }
13589
13590    #[test]
13591    fn common_number_formats_project_cached_values_deterministically() {
13592        for (code, value, expected) in [
13593            ("General", 0.25, "0.25"),
13594            ("0", 12.6, "13"),
13595            ("0.0", 12.64, "12.6"),
13596            ("0.00", -12.645, "-12.64"),
13597            ("0%", 0.25, "25%"),
13598            ("0.0%", 0.256, "25.6%"),
13599            ("0.00%", 0.256, "25.60%"),
13600        ] {
13601            let format = NumberFormat::new(code.to_owned(), false).unwrap();
13602            assert_eq!(format.format_value(value).unwrap(), expected, "{code}");
13603        }
13604    }
13605
13606    #[test]
13607    fn malformed_data_labels_and_number_formats_return_errors_without_panicking() {
13608        let cases = [
13609            format!(
13610                r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="1"/><c:showVal val="0"/></c:dLbls>"#
13611            ),
13612            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:showVal val="yes"/></c:dLbls>"#),
13613            format!(r#"<c:dLbls xmlns:c="{C_NS}"><c:dLblPos val="middle"/></c:dLbls>"#),
13614            format!(
13615                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="" sourceLinked="1"/></c:dLbls>"#
13616            ),
13617            format!(
13618                r#"<c:dLbls xmlns:c="{C_NS}"><c:numFmt formatCode="0" sourceLinked="maybe"/></c:dLbls>"#
13619            ),
13620        ];
13621        for xml in cases {
13622            let result = std::panic::catch_unwind(|| CT_DLbls::from_xml(xml.as_bytes()));
13623            assert!(result.is_ok(), "data-label parser panicked for {xml}");
13624            assert!(result.unwrap().is_err(), "malformed labels parsed: {xml}");
13625        }
13626
13627        let invalid_code = NumberFormat::new("bad\u{0}code".to_owned(), false);
13628        assert!(invalid_code.is_err());
13629        let unsupported = NumberFormat::new("#,##0".to_owned(), false).unwrap();
13630        assert!(unsupported.format_value(12.0).is_err());
13631        let general = NumberFormat::new("General".to_owned(), false).unwrap();
13632        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
13633            assert!(general.format_value(value).is_err());
13634        }
13635
13636        let invalid_separator = CT_DLbls {
13637            separator: Some("bad\u{0}separator".to_owned()),
13638            ..CT_DLbls::default()
13639        };
13640        assert!(invalid_separator.to_xml().is_err());
13641    }
13642
13643    #[test]
13644    fn data_labels_preserve_point_overrides_and_extensions_byte_for_byte() {
13645        let xml = format!(
13646            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>"#
13647        );
13648        let parsed = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
13649        let written = parsed.to_xml().unwrap();
13650        for raw in [
13651            br#"<!--before-->"#.as_slice(),
13652            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(),
13653            br#"<?after-point?>"#.as_slice(),
13654            br#"<q:spPr><x:collection-shape/></q:spPr>"#.as_slice(),
13655            br#"<q:txPr><x:collection-text/></q:txPr>"#.as_slice(),
13656            br#"<q:showLeaderLines val="1"/>"#.as_slice(),
13657            br#"<q:leaderLines><q:spPr><x:leader-shape/></q:spPr></q:leaderLines>"#.as_slice(),
13658            br#"<q:extLst><q:ext uri="collection"><x:tail/></q:ext></q:extLst>"#.as_slice(),
13659            br#"<!--after-->"#.as_slice(),
13660        ] {
13661            assert!(
13662                written.windows(raw.len()).any(|window| window == raw),
13663                "preserved data-label bytes changed: {}",
13664                String::from_utf8_lossy(raw)
13665            );
13666        }
13667        assert_eq!(parsed, CT_DLbls::from_xml(&written).unwrap());
13668    }
13669
13670    #[test]
13671    fn every_corpus_data_label_collection_round_trips_structurally() {
13672        let Some(corpus) = require_or_skip_corpus() else {
13673            return;
13674        };
13675        verify_fetched_corpus(&corpus);
13676        let mut label_collections = 0usize;
13677        let mut axis_number_formats = 0usize;
13678        for path in manifest_paths() {
13679            let package = OpcPackage::open(corpus.join(path))
13680                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
13681            for (part, xml) in &package.parts {
13682                if !is_chart_part(part) {
13683                    continue;
13684                }
13685                let chart = CT_ChartSpace::from_xml(xml)
13686                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
13687                for series in chart.chart.plot_area.series().unwrap_or_else(|error| {
13688                    panic!("{path} {part}: series projection failed: {error}")
13689                }) {
13690                    if let Some(labels) = series.data_labels {
13691                        let written = labels.to_xml().unwrap_or_else(|error| {
13692                            panic!("{path} {part}: label write failed: {error}")
13693                        });
13694                        let reparsed = CT_DLbls::from_xml(&written).unwrap_or_else(|error| {
13695                            panic!("{path} {part}: written labels parse failed: {error}")
13696                        });
13697                        assert_eq!(labels, reparsed, "{path} {part}: labels changed");
13698                        label_collections += 1;
13699                    }
13700                }
13701                for axis in chart.chart.plot_area.axes().unwrap_or_else(|error| {
13702                    panic!("{path} {part}: axis projection failed: {error}")
13703                }) {
13704                    if axis.number_format.is_some() {
13705                        let written = axis.to_xml().unwrap_or_else(|error| {
13706                            panic!("{path} {part}: axis write failed: {error}")
13707                        });
13708                        let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
13709                            panic!("{path} {part}: written axis parse failed: {error}")
13710                        });
13711                        assert_eq!(axis, reparsed, "{path} {part}: axis changed");
13712                        axis_number_formats += 1;
13713                    }
13714                }
13715            }
13716        }
13717        assert!(label_collections > 0, "the pinned corpus has no c:dLbls");
13718        assert!(
13719            axis_number_formats > 0,
13720            "the pinned corpus has no axis c:numFmt"
13721        );
13722        eprintln!(
13723            "ChartML data-label corpus gate checked {label_collections} collections and {axis_number_formats} axis number formats"
13724        );
13725    }
13726
13727    fn full_axis(local: &str, position: &str) -> String {
13728        format!(
13729            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}>"#
13730        )
13731    }
13732
13733    #[test]
13734    fn axis_id_pairs_are_reciprocal() {
13735        assert_eq!(
13736            AxisId::new(i64::from(i32::MIN)).unwrap().value(),
13737            i64::from(i32::MIN)
13738        );
13739        assert_eq!(
13740            AxisId::new(i64::from(u32::MAX)).unwrap().value(),
13741            i64::from(u32::MAX)
13742        );
13743        assert!(AxisId::new(i64::from(i32::MIN) - 1).is_err());
13744        assert!(AxisId::new(i64::from(u32::MAX) + 1).is_err());
13745
13746        let valid = chart_with_axes(&[
13747            minimal_axis("catAx", "-1884094432", "b", "-1884097184"),
13748            minimal_axis("valAx", "-1884097184", "l", "-1884094432"),
13749        ]);
13750        let axes = CT_ChartSpace::from_xml(valid.as_bytes())
13751            .unwrap()
13752            .chart
13753            .plot_area
13754            .axes()
13755            .unwrap();
13756        assert_eq!(axes.len(), 2);
13757        assert_eq!(axes[0].id.value(), -1_884_094_432);
13758        assert_eq!(axes[0].cross_axis, axes[1].id);
13759        assert_eq!(axes[1].cross_axis, axes[0].id);
13760
13761        let invalid_sets = [
13762            vec![
13763                minimal_axis("catAx", "1", "b", "2"),
13764                minimal_axis("valAx", "1", "l", "2"),
13765            ],
13766            vec![minimal_axis("catAx", "1", "b", "1")],
13767            vec![minimal_axis("catAx", "1", "b", "2")],
13768            vec![
13769                minimal_axis("catAx", "1", "b", "2"),
13770                minimal_axis("valAx", "2", "l", "3"),
13771                minimal_axis("serAx", "3", "r", "2"),
13772            ],
13773        ];
13774        for axes in invalid_sets {
13775            let chart = CT_ChartSpace::from_xml(chart_with_axes(&axes).as_bytes()).unwrap();
13776            assert!(chart.chart.plot_area.axes().is_err());
13777        }
13778
13779        let empty = CT_ChartSpace::from_xml(chart_with_axes(&[]).as_bytes()).unwrap();
13780        assert!(empty.chart.plot_area.axes().unwrap().is_empty());
13781    }
13782
13783    #[test]
13784    fn all_axis_forms_write_fixed_prefixes_in_schema_order() {
13785        for (local, kind, position) in [
13786            ("catAx", AxisKind::Category, AxisPosition::Bottom),
13787            ("valAx", AxisKind::Value, AxisPosition::Left),
13788            ("dateAx", AxisKind::Date, AxisPosition::Top),
13789            ("serAx", AxisKind::Series, AxisPosition::Right),
13790        ] {
13791            let xml = full_axis(local, position.as_str());
13792            let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
13793            assert_eq!(parsed.kind, kind);
13794            assert_eq!(parsed.id.value(), -1_125_255_920);
13795            assert_eq!(parsed.scaling.log_base, Some(10.0));
13796            assert_eq!(parsed.scaling.orientation, Orientation::MaxMin);
13797            assert_eq!(parsed.scaling.maximum, Some(100.0));
13798            assert_eq!(parsed.scaling.minimum, Some(-10.0));
13799            assert!(parsed.deleted);
13800            assert_eq!(parsed.position, position);
13801            assert!(parsed.major_gridlines.is_some());
13802            assert!(parsed.minor_gridlines.is_some());
13803            assert!(parsed.title.is_some());
13804            assert_eq!(parsed.number_format.as_ref().unwrap().format_code, "0.0");
13805            assert_eq!(parsed.major_tick_mark, TickMark::Cross);
13806            assert_eq!(parsed.minor_tick_mark, TickMark::Inside);
13807            assert_eq!(parsed.tick_label_position, TickLabelPosition::High);
13808            assert!(parsed.sp_pr.is_some());
13809            assert!(parsed.tx_pr.is_some());
13810            assert_eq!(parsed.cross_axis.value(), -1_004_303_696);
13811
13812            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
13813            assert!(written.starts_with(&format!(
13814                "<c:{local} xmlns:c=\"{C_NS}\" xmlns:a=\"{A_NS}\" xmlns:r=\"{R_NS}\""
13815            )));
13816            assert!(written.contains("<c:axId val=\"-1125255920\""));
13817            let mut cursor = 0usize;
13818            for tag in [
13819                "<c:axId",
13820                "<c:scaling",
13821                "<c:delete",
13822                "<c:axPos",
13823                "<c:majorGridlines",
13824                "<c:minorGridlines",
13825                "<c:title",
13826                "<c:numFmt",
13827                "<c:majorTickMark",
13828                "<c:minorTickMark",
13829                "<c:tickLblPos",
13830                "<c:spPr",
13831                "<c:txPr",
13832                "<c:crossAx",
13833            ] {
13834                let position = written[cursor..]
13835                    .find(tag)
13836                    .unwrap_or_else(|| panic!("missing ordered axis tag {tag}"));
13837                cursor += position + tag.len();
13838            }
13839            assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
13840        }
13841
13842        let mut constructed = Axis::new(
13843            AxisKind::Value,
13844            AxisId::new(1).unwrap(),
13845            AxisPosition::Left,
13846            AxisId::new(2).unwrap(),
13847        );
13848        assert_eq!(
13849            constructed,
13850            Axis::from_xml(&constructed.to_xml().unwrap()).unwrap()
13851        );
13852        constructed.kind = AxisKind::Category;
13853        constructed.id = AxisId::new(3).unwrap();
13854        constructed.cross_axis = AxisId::new(4).unwrap();
13855        constructed.scaling.minimum = Some(0.0);
13856        constructed.scaling.maximum = Some(10.0);
13857        let written = constructed.to_xml().unwrap();
13858        assert!(written.starts_with(b"<c:catAx"));
13859        assert_eq!(constructed, Axis::from_xml(&written).unwrap());
13860    }
13861
13862    #[test]
13863    fn axis_equality_normalizes_ids_without_losing_lexical_preservation() {
13864        let padded = Axis::from_xml(
13865            standalone_axis(
13866                "catAx",
13867                "<c:axId val=\"01\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"002\"/>",
13868            )
13869            .as_bytes(),
13870        )
13871        .unwrap();
13872        let normalized = Axis::from_xml(
13873            standalone_axis(
13874                "catAx",
13875                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13876            )
13877            .as_bytes(),
13878        )
13879        .unwrap();
13880        assert_eq!(padded, normalized);
13881
13882        let preserved = String::from_utf8(padded.to_xml().unwrap()).unwrap();
13883        assert!(preserved.contains("<c:axId val=\"01\""));
13884        assert!(preserved.contains("<c:crossAx val=\"002\""));
13885
13886        let mut mutated = padded;
13887        mutated.id = AxisId::new(3).unwrap();
13888        mutated.cross_axis = AxisId::new(4).unwrap();
13889        let rewritten = String::from_utf8(mutated.to_xml().unwrap()).unwrap();
13890        assert!(rewritten.contains("<c:axId val=\"3\""));
13891        assert!(rewritten.contains("<c:crossAx val=\"4\""));
13892        assert!(!rewritten.contains("val=\"01\""));
13893        assert!(!rewritten.contains("val=\"002\""));
13894    }
13895
13896    #[test]
13897    fn malformed_axis_values_return_errors_without_panicking() {
13898        let cases = [
13899            standalone_axis(
13900                "catAx",
13901                "<c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13902            ),
13903            standalone_axis(
13904                "catAx",
13905                "<c:axId val=\"1\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13906            ),
13907            standalone_axis(
13908                "catAx",
13909                "<c:axId val=\"1\"/><c:scaling/><c:crossAx val=\"2\"/>",
13910            ),
13911            standalone_axis(
13912                "catAx",
13913                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/>",
13914            ),
13915            standalone_axis(
13916                "catAx",
13917                "<c:axId val=\"-2147483649\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13918            ),
13919            standalone_axis(
13920                "catAx",
13921                "<c:axId val=\"4294967296\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13922            ),
13923            standalone_axis(
13924                "catAx",
13925                "<c:axId val=\"1\"/><c:scaling><c:logBase val=\"1\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13926            ),
13927            standalone_axis(
13928                "catAx",
13929                "<c:axId val=\"1\"/><c:scaling><c:max val=\"NaN\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13930            ),
13931            standalone_axis(
13932                "catAx",
13933                "<c:axId val=\"1\"/><c:scaling><c:max val=\"1\"/><c:min val=\"2\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13934            ),
13935            standalone_axis(
13936                "catAx",
13937                "<c:axId val=\"1\"/><c:scaling><c:orientation val=\"sideways\"/></c:scaling><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13938            ),
13939            standalone_axis(
13940                "catAx",
13941                "<c:axId val=\"1\"/><c:scaling/><c:delete val=\"yes\"/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13942            ),
13943            standalone_axis(
13944                "catAx",
13945                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"middle\"/><c:crossAx val=\"2\"/>",
13946            ),
13947            standalone_axis(
13948                "catAx",
13949                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:majorTickMark val=\"near\"/><c:crossAx val=\"2\"/>",
13950            ),
13951            standalone_axis(
13952                "catAx",
13953                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:tickLblPos val=\"middle\"/><c:crossAx val=\"2\"/>",
13954            ),
13955            standalone_axis(
13956                "catAx",
13957                "<c:axId val=\"1\"/><c:axId val=\"2\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
13958            ),
13959            standalone_axis(
13960                "catAx",
13961                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt sourceLinked=\"1\"/><c:crossAx val=\"2\"/>",
13962            ),
13963            standalone_axis(
13964                "catAx",
13965                "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:numFmt formatCode=\"0\" sourceLinked=\"maybe\"/><c:crossAx val=\"2\"/>",
13966            ),
13967        ];
13968        for xml in cases {
13969            let result = std::panic::catch_unwind(|| Axis::from_xml(xml.as_bytes()));
13970            assert!(result.is_ok(), "axis parser panicked for {xml}");
13971            assert!(result.unwrap().is_err(), "malformed axis parsed: {xml}");
13972        }
13973
13974        let mut axis = Axis::new(
13975            AxisKind::Value,
13976            AxisId::new(1).unwrap(),
13977            AxisPosition::Left,
13978            AxisId::new(2).unwrap(),
13979        );
13980        axis.scaling.maximum = Some(f64::INFINITY);
13981        assert!(axis.to_xml().is_err());
13982    }
13983
13984    #[test]
13985    fn axes_preserve_unmodelled_children_byte_for_byte() {
13986        let xml = format!(
13987            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>"#
13988        );
13989        let parsed = Axis::from_xml(xml.as_bytes()).unwrap();
13990        let mut relabelled = parsed.clone();
13991        relabelled.kind = AxisKind::Value;
13992        assert!(
13993            relabelled.to_xml().is_err(),
13994            "a parsed category axis must not retain its tail under a value-axis root"
13995        );
13996        let written = parsed.to_xml().unwrap();
13997        for raw in [
13998            br#"<!--before-->"#.as_slice(),
13999            br#"<?after-id?>"#.as_slice(),
14000            br#"<!--scale-->"#.as_slice(),
14001            br#"<q:extLst><q:ext uri="scale"><x:data/></q:ext></q:extLst>"#.as_slice(),
14002            br#"<x:grid/>"#.as_slice(),
14003            br#"<x:title/>"#.as_slice(),
14004            br#"<q:crosses val="autoZero"/>"#.as_slice(),
14005            br#"<q:auto val="1"><x:auto/></q:auto>"#.as_slice(),
14006            br#"<q:extLst><q:ext uri="axis"><x:tail/></q:ext></q:extLst>"#.as_slice(),
14007            br#"<!--after-->"#.as_slice(),
14008        ] {
14009            assert!(
14010                written.windows(raw.len()).any(|window| window == raw),
14011                "preserved axis bytes changed: {}",
14012                String::from_utf8_lossy(raw)
14013            );
14014        }
14015        let written = String::from_utf8(written).unwrap();
14016        assert!(written.starts_with("<c:catAx"));
14017        assert!(written.contains("x:keep=\"axis\""));
14018        assert!(written.contains("x:keep=\"id\""));
14019        assert_eq!(parsed, Axis::from_xml(written.as_bytes()).unwrap());
14020    }
14021
14022    #[test]
14023    fn every_corpus_axis_round_trips_structurally() {
14024        let Some(corpus) = require_or_skip_corpus() else {
14025            return;
14026        };
14027        verify_fetched_corpus(&corpus);
14028        let mut axis_count = 0usize;
14029        let mut chart_parts = 0usize;
14030        let mut kinds = HashSet::new();
14031        for path in manifest_paths() {
14032            let package = OpcPackage::open(corpus.join(path))
14033                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14034            for (part, xml) in &package.parts {
14035                if !is_chart_part(part) {
14036                    continue;
14037                }
14038                let chart = CT_ChartSpace::from_xml(xml)
14039                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14040                let axes = chart.chart.plot_area.axes().unwrap_or_else(|error| {
14041                    panic!("{path} {part}: axis projection failed: {error}")
14042                });
14043                for axis in axes {
14044                    kinds.insert(axis.kind);
14045                    let written = axis.to_xml().unwrap_or_else(|error| {
14046                        panic!("{path} {part}: axis write failed: {error}")
14047                    });
14048                    let reparsed = Axis::from_xml(&written).unwrap_or_else(|error| {
14049                        panic!("{path} {part}: written axis parse failed: {error}")
14050                    });
14051                    assert_eq!(axis, reparsed, "{path} {part}: axis model changed");
14052                    axis_count += 1;
14053                }
14054                chart_parts += 1;
14055            }
14056        }
14057        assert!(
14058            axis_count > 0,
14059            "the pinned corpus contained no ChartML axes"
14060        );
14061        assert!(kinds.contains(&AxisKind::Category));
14062        assert!(kinds.contains(&AxisKind::Value));
14063
14064        for kind in ["dateAx", "serAx"] {
14065            let parsed = Axis::from_xml(
14066                standalone_axis(
14067                    kind,
14068                    "<c:axId val=\"1\"/><c:scaling/><c:axPos val=\"b\"/><c:crossAx val=\"2\"/>",
14069                )
14070                .as_bytes(),
14071            )
14072            .unwrap();
14073            assert_eq!(parsed, Axis::from_xml(&parsed.to_xml().unwrap()).unwrap());
14074        }
14075        eprintln!(
14076            "ChartML axis corpus gate checked {axis_count} axes across {chart_parts} chart parts"
14077        );
14078    }
14079
14080    #[test]
14081    fn series_formula_and_cache_are_consistent_with_one_source() {
14082        let values = NumericData::new(
14083            "'Sales 24'!$B$2:$B$4".to_owned(),
14084            "0.0".to_owned(),
14085            vec![4.25, 8.5, 17.0],
14086        )
14087        .unwrap();
14088        let mut series = Series::new(3, 1, values);
14089        series.categories = Some(AxisData::String(
14090            StringRef::new(
14091                "'Sales 24'!$A$2:$A$4".to_owned(),
14092                vec!["North".to_owned(), "South".to_owned(), "West".to_owned()],
14093            )
14094            .unwrap(),
14095        ));
14096
14097        let written = String::from_utf8(series.to_xml().unwrap()).unwrap();
14098        assert!(written.contains("<c:f>&apos;Sales 24&apos;!$B$2:$B$4</c:f>"));
14099        assert!(written.contains("<c:formatCode>0.0</c:formatCode>"));
14100        assert_eq!(written.matches("<c:ptCount val=\"3\"").count(), 2);
14101        for (index, value) in ["4.25", "8.5", "17"].iter().enumerate() {
14102            assert!(written.contains(&format!("<c:pt idx=\"{index}\"><c:v>{value}</c:v></c:pt>")));
14103        }
14104        let reparsed = Series::from_xml(written.as_bytes()).unwrap();
14105        assert_eq!(reparsed.index, series.index);
14106        assert_eq!(reparsed.order, series.order);
14107        match (&reparsed.categories, &series.categories) {
14108            (Some(AxisData::String(left)), Some(AxisData::String(right))) => {
14109                assert_eq!(left.formula, right.formula);
14110                assert_eq!(left.values, right.values);
14111            }
14112            _ => panic!("expected string categories"),
14113        }
14114        assert_eq!(reparsed.values.formula, series.values.formula);
14115        assert_eq!(reparsed.values.format_code, series.values.format_code);
14116        assert_eq!(reparsed.values.values, series.values.values);
14117    }
14118
14119    #[test]
14120    fn string_and_numeric_references_write_fixed_prefixes_in_schema_order() {
14121        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>"#;
14122        let string_ref = StringRef::from_xml(string_xml).unwrap();
14123        let written = String::from_utf8(string_ref.to_xml().unwrap()).unwrap();
14124        assert!(written.starts_with("<c:strRef xmlns:c="));
14125        assert!(written.find("<c:f>").unwrap() < written.find("<c:strCache>").unwrap());
14126        assert!(written.find("<c:ptCount").unwrap() < written.find("<c:pt idx=").unwrap());
14127
14128        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>"#;
14129        let numeric = NumericData::from_xml(numeric_xml).unwrap();
14130        let written = String::from_utf8(numeric.to_xml().unwrap()).unwrap();
14131        let positions: Vec<_> = [
14132            "<c:f>",
14133            "<c:numCache>",
14134            "<c:formatCode>",
14135            "<c:ptCount",
14136            "<c:pt idx=",
14137        ]
14138        .iter()
14139        .map(|tag| written.find(tag).unwrap())
14140        .collect();
14141        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14142        assert_eq!(numeric, NumericData::from_xml(written.as_bytes()).unwrap());
14143    }
14144
14145    #[test]
14146    fn mixed_chartml_aliases_resolve_by_namespace_uri() {
14147        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>"#;
14148        let parsed = Series::from_xml(xml).unwrap();
14149        assert_eq!(parsed.index, 4);
14150        assert_eq!(parsed.order, 2);
14151        assert_eq!(parsed.values.values, vec![1.5, 2.5]);
14152        assert!(matches!(parsed.categories, Some(AxisData::String(_))));
14153        assert_eq!(parsed, Series::from_xml(&parsed.to_xml().unwrap()).unwrap());
14154    }
14155
14156    #[test]
14157    fn plot_area_series_ignores_inherited_foreign_plot_aliases() {
14158        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>"#;
14159        let chart = CT_ChartSpace::from_xml(xml).unwrap();
14160        let series = chart.chart.plot_area.series().unwrap();
14161        assert_eq!(series.len(), 1);
14162        assert_eq!(series[0].index, 1);
14163        let written = String::from_utf8(series[0].to_xml().unwrap()).unwrap();
14164        assert!(
14165            written.contains(r#"xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart""#)
14166        );
14167        assert!(written.contains(r#"xmlns:x="urn:producer""#));
14168        assert!(written.contains("<q:marker><x:data/></q:marker>"));
14169        assert_eq!(series[0], Series::from_xml(written.as_bytes()).unwrap());
14170
14171        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>"#;
14172        let chart = CT_ChartSpace::from_xml(conflicting).unwrap();
14173        assert!(chart.chart.plot_area.series().is_err());
14174    }
14175
14176    #[test]
14177    fn malformed_series_and_cache_values_return_errors_without_panicking() {
14178        let cases: &[&[u8]] = &[
14179            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>"#,
14180            br#"<c:ser xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:idx val="0"/><c:order val="0"/></c:ser>"#,
14181            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>"#,
14182            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>"#,
14183            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>"#,
14184            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>"#,
14185            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>"#,
14186            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>"#,
14187            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>"#,
14188            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>"#,
14189            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>"#,
14190            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>"#,
14191            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>"#,
14192            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>"#,
14193            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>"#,
14194            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>"#,
14195            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>"#,
14196        ];
14197        for xml in cases {
14198            let result = std::panic::catch_unwind(|| Series::from_xml(xml));
14199            assert!(result.is_ok(), "series parser panicked");
14200            assert!(
14201                result.unwrap().is_err(),
14202                "malformed series parsed: {}",
14203                String::from_utf8_lossy(xml)
14204            );
14205        }
14206        assert!(
14207            NumericData::new(
14208                "S!$A$1".to_owned(),
14209                "General".to_owned(),
14210                vec![f64::INFINITY]
14211            )
14212            .is_err()
14213        );
14214    }
14215
14216    #[test]
14217    fn chart_text_rejects_xml_forbidden_characters_and_escapes_metacharacters() {
14218        assert!(StringRef::new("S!$A$1\u{1}".to_owned(), Vec::new()).is_err());
14219        assert!(StringRef::new("S!$A$1".to_owned(), vec!["invalid\u{1}".to_owned()]).is_err());
14220        assert!(
14221            NumericData::new("S!$A$1\u{1}".to_owned(), "General".to_owned(), Vec::new()).is_err()
14222        );
14223        assert!(
14224            NumericData::new("S!$A$1".to_owned(), "General\u{1}".to_owned(), Vec::new()).is_err()
14225        );
14226
14227        let mut strings = StringRef::new("S!$A$1".to_owned(), vec!["valid".to_owned()]).unwrap();
14228        strings.formula = "invalid\u{1}".to_owned();
14229        assert!(strings.to_xml().is_err());
14230        strings.formula = "S!$A$1".to_owned();
14231        strings.values[0] = "invalid\u{1}".to_owned();
14232        assert!(strings.to_xml().is_err());
14233
14234        let mut numbers =
14235            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap();
14236        numbers.formula = "invalid\u{1}".to_owned();
14237        assert!(numbers.to_xml().is_err());
14238        numbers.formula = "S!$A$1".to_owned();
14239        numbers.format_code = "invalid\u{1}".to_owned();
14240        assert!(numbers.to_xml().is_err());
14241
14242        let strings =
14243            StringRef::new("S!$A$1&".to_owned(), vec!["North < West & East".to_owned()]).unwrap();
14244        let written = String::from_utf8(strings.to_xml().unwrap()).unwrap();
14245        assert!(written.contains("S!$A$1&amp;"));
14246        assert!(written.contains("North &lt; West &amp; East"));
14247    }
14248
14249    #[test]
14250    fn series_preserves_unmodelled_children_byte_for_byte() {
14251        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>"#;
14252        let parsed = Series::from_xml(xml).unwrap();
14253        let written = parsed.to_xml().unwrap();
14254        for raw in [
14255            br#"<!--before-->"#.as_slice(),
14256            br#"<x:between/>"#.as_slice(),
14257            br#"<q:dPt x:id="one"><q:idx val="0"/></q:dPt>"#.as_slice(),
14258            br#"<q:trendline x:id="trend"/>"#.as_slice(),
14259            br#"<q:extLst><q:ext uri="keep"><x:data/></q:ext></q:extLst>"#.as_slice(),
14260            br#"<!--point-->"#.as_slice(),
14261            br#"<x:cacheExt/>"#.as_slice(),
14262        ] {
14263            assert!(
14264                written.windows(raw.len()).any(|window| window == raw),
14265                "preserved series bytes changed: {}",
14266                String::from_utf8_lossy(raw)
14267            );
14268        }
14269        let written = String::from_utf8(written.clone()).unwrap();
14270        assert!(written.contains(r#"x:keep="series""#));
14271        assert!(written.contains(r#"x:keep="formula""#));
14272        assert!(written.contains("<c:dLbls"));
14273        assert!(written.contains(r#"x:id="labels""#));
14274        assert!(parsed.data_labels.is_some());
14275        assert_eq!(parsed, Series::from_xml(written.as_bytes()).unwrap());
14276    }
14277
14278    #[test]
14279    fn public_series_edits_do_not_duplicate_or_drop_preserved_payloads() {
14280        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>"#;
14281        let parsed = Series::from_xml(opaque_xml).unwrap();
14282
14283        let mut edited = parsed.clone();
14284        edited.name = Some(StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap());
14285        assert!(edited.to_xml().is_err());
14286
14287        let mut edited = parsed.clone();
14288        edited.categories = Some(AxisData::Numeric(
14289            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap(),
14290        ));
14291        assert!(edited.to_xml().is_err());
14292
14293        let mut edited = parsed;
14294        edited.bubble_size =
14295            Some(NumericData::new("S!$A$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14296        assert!(edited.to_xml().is_err());
14297
14298        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>"#;
14299        let parsed = Series::from_xml(cache_xml).unwrap();
14300        let tail = "<c:extLst><c:ext uri=\"keep\"><x:data/></c:ext></c:extLst>";
14301
14302        let mut shortened = parsed.clone();
14303        shortened.values.values.truncate(1);
14304        let written = String::from_utf8(shortened.to_xml().unwrap()).unwrap();
14305        assert!(!written.contains("<c:pt idx=\"1\""));
14306        assert!(written.find("<c:pt idx=\"0\"").unwrap() < written.find(tail).unwrap());
14307        assert!(Series::from_xml(written.as_bytes()).is_ok());
14308
14309        let mut grown = parsed;
14310        grown.values.values.push(3.0);
14311        let written = String::from_utf8(grown.to_xml().unwrap()).unwrap();
14312        assert!(written.find("<c:pt idx=\"2\"").unwrap() < written.find(tail).unwrap());
14313        assert!(Series::from_xml(written.as_bytes()).is_ok());
14314
14315        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>"#;
14316        let mut edited = Series::from_xml(ordered_xml).unwrap();
14317        edited.name = Some(StringRef::new("S!$B$1".to_owned(), Vec::new()).unwrap());
14318        edited.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14319        edited.categories = Some(AxisData::String(
14320            StringRef::new("S!$A$1".to_owned(), Vec::new()).unwrap(),
14321        ));
14322        edited.bubble_size =
14323            Some(NumericData::new("S!$C$1".to_owned(), "General".to_owned(), Vec::new()).unwrap());
14324        let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
14325        let positions: Vec<_> = [
14326            "<c:tx>",
14327            "<c:spPr",
14328            "<c:marker>",
14329            "<c:cat>",
14330            "<c:val>",
14331            "<c:bubbleSize>",
14332            "<c:extLst>",
14333        ]
14334        .iter()
14335        .map(|tag| written.find(tag).unwrap())
14336        .collect();
14337        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14338        assert!(Series::from_xml(written.as_bytes()).is_ok());
14339    }
14340
14341    #[test]
14342    fn every_corpus_series_round_trips_structurally() {
14343        let Some(corpus) = require_or_skip_corpus() else {
14344            return;
14345        };
14346        verify_fetched_corpus(&corpus);
14347        let mut series_count = 0usize;
14348        let mut chart_parts = 0usize;
14349        for path in manifest_paths() {
14350            let package = OpcPackage::open(corpus.join(path))
14351                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14352            for (part, xml) in &package.parts {
14353                if !is_chart_part(part) {
14354                    continue;
14355                }
14356                let chart = CT_ChartSpace::from_xml(xml)
14357                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14358                let parsed_series =
14359                    chart.chart.plot_area.series().unwrap_or_else(|error| {
14360                        panic!("{path} {part}: series parse failed: {error}")
14361                    });
14362                for series in parsed_series {
14363                    let written = series.to_xml().unwrap_or_else(|error| {
14364                        panic!("{path} {part}: series write failed: {error}")
14365                    });
14366                    let reparsed = Series::from_xml(&written).unwrap_or_else(|error| {
14367                        panic!("{path} {part}: written series parse failed: {error}")
14368                    });
14369                    assert_eq!(series, reparsed, "{path} {part}: series model changed");
14370                    series_count += 1;
14371                }
14372                chart_parts += 1;
14373            }
14374        }
14375        assert!(
14376            series_count > 0,
14377            "the pinned corpus contained no supported series"
14378        );
14379        eprintln!(
14380            "ChartML series corpus gate checked {series_count} series across {chart_parts} chart parts"
14381        );
14382    }
14383
14384    #[test]
14385    fn chart_space_reads_aliases_and_writes_fixed_prefixes_in_schema_order() {
14386        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>"#;
14387        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14388        assert!(!parsed.chart.auto_title_deleted);
14389        assert!(!parsed.chart.plot_vis_only);
14390        assert_eq!(parsed.chart.disp_blanks_as, DispBlanksAs::Span);
14391        assert!(parsed.chart.title.is_some());
14392        assert!(parsed.chart.legend.is_some());
14393        assert!(parsed.sp_pr.is_some());
14394        assert!(parsed.tx_pr.is_some());
14395
14396        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14397        assert!(written.starts_with(
14398            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""#
14399        ));
14400        for tag in [
14401            "<c:chart>",
14402            "<c:title>",
14403            "<c:plotArea>",
14404            "<c:legend>",
14405            "<c:plotVisOnly",
14406            "<c:dispBlanksAs",
14407            "<c:spPr>",
14408            "<c:txPr>",
14409        ] {
14410            assert!(written.contains(tag), "missing fixed-prefix tag {tag}");
14411        }
14412        let positions: Vec<_> = [
14413            "<c:title>",
14414            "<c:plotArea>",
14415            "<c:legend>",
14416            "<c:plotVisOnly",
14417            "<c:dispBlanksAs",
14418            "<c:spPr>",
14419            "<c:txPr>",
14420        ]
14421        .iter()
14422        .map(|tag| written.find(tag).unwrap())
14423        .collect();
14424        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14425        assert!(written.contains("<a:solidFill><a:srgbClr"));
14426        assert!(written.contains("<a:bodyPr/><a:p/>"));
14427        assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
14428    }
14429
14430    #[test]
14431    fn public_core_edits_keep_preserved_children_in_their_schema_slots() {
14432        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>"#;
14433        let mut parsed = CT_ChartSpace::from_xml(xml).unwrap();
14434        parsed.chart.auto_title_deleted = true;
14435        parsed.sp_pr = Some(CT_ShapeProperties::from_xml(br#"<a:spPr/>"#).unwrap());
14436        parsed.tx_pr =
14437            Some(CT_TextBody::from_xml(br#"<a:txBody><a:bodyPr/><a:p/></a:txBody>"#).unwrap());
14438        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
14439        let positions: Vec<_> = [
14440            "<c:autoTitleDeleted",
14441            "<c:pivotFmts>",
14442            "<c:plotArea",
14443            "<c:spPr",
14444            "<c:txPr",
14445            "<c:externalData",
14446        ]
14447        .iter()
14448        .map(|tag| written.find(tag).unwrap())
14449        .collect();
14450        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
14451        assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
14452    }
14453
14454    #[test]
14455    fn malformed_core_chart_values_return_errors_without_panicking() {
14456        let cases: &[&[u8]] = &[
14457            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#,
14458            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart/></c:chartSpace>"#,
14459            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:plotArea/></c:chart></c:chartSpace>"#,
14460            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:autoTitleDeleted val="yes"/><c:plotArea/></c:chart></c:chartSpace>"#,
14461            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:plotArea/><c:dispBlanksAs val="empty"/></c:chart></c:chartSpace>"#,
14462            br#"<c:chartSpace xmlns:c="urn:wrong"><c:chart><c:plotArea/></c:chart></c:chartSpace>"#,
14463            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:x="urn:foreign"><x:chart><x:plotArea/></x:chart></c:chartSpace>"#,
14464            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>"#,
14465            br#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart><c:title/><c:title/><c:plotArea/></c:chart></c:chartSpace>"#,
14466            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>"#,
14467            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>"#,
14468            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>"#,
14469        ];
14470        for xml in cases {
14471            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml));
14472            assert!(result.is_ok(), "ChartML parser panicked");
14473            assert!(
14474                result.unwrap().is_err(),
14475                "malformed ChartML parsed: {}",
14476                String::from_utf8_lossy(xml)
14477            );
14478        }
14479    }
14480
14481    #[test]
14482    fn point_label_overrides_render_without_changing_preserved_xml() {
14483        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>"#;
14484        let plot = bar_plot("").replace(
14485            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14486            &format!(
14487                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>"#
14488            ),
14489        );
14490        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot).as_bytes()).unwrap();
14491        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14492        let Plot::Bar { data_labels, .. } = plot else {
14493            panic!("expected bar plot");
14494        };
14495        let labels = data_labels.as_ref().unwrap();
14496        let written = labels.to_xml().unwrap();
14497        assert!(
14498            written
14499                .windows(raw_overrides.len())
14500                .any(|window| window == raw_overrides.as_bytes())
14501        );
14502        let overrides = super::point_label_overrides(labels).unwrap();
14503        assert_eq!(overrides.keys().copied().collect::<Vec<_>>(), vec![1]);
14504
14505        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14506        let mut rendered = Vec::new();
14507        super::render_data_labels(
14508            plot,
14509            chart_bounds(),
14510            DispBlanksAs::Gap,
14511            super::GeometryOptions::default(),
14512            &mut fonts,
14513            &mut rendered,
14514        )
14515        .unwrap();
14516        let labels: Vec<_> = rendered
14517            .iter()
14518            .filter_map(|element| match element {
14519                PositionedElement::Text(run) => Some(run),
14520                _ => None,
14521            })
14522            .collect();
14523        assert_eq!(labels.len(), 2);
14524        assert_eq!(labels[0].text, "North, 1");
14525        assert_eq!(labels[1].text, "South / 2.0");
14526        assert_point_near(labels[0].origin, Point { x: 50.0, y: 105.0 });
14527        assert_point_near(labels[1].origin, Point { x: 158.0, y: 70.0 });
14528        assert_eq!(
14529            CT_DLbls::from_xml(&written).unwrap().to_xml().unwrap(),
14530            written
14531        );
14532    }
14533
14534    #[test]
14535    fn malformed_point_label_overrides_return_contextual_errors() {
14536        let cases = [
14537            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:showVal val="1"/></q:dLbl></q:dLbls>"#,
14538            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>"#,
14539            r#"<q:dLbls xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/chart"><q:dLbl><q:idx val="point"/></q:dLbl></q:dLbls>"#,
14540            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>"#,
14541            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>"#,
14542            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>"#,
14543            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>"#,
14544        ];
14545        for xml in cases {
14546            let labels = CT_DLbls::from_xml(xml.as_bytes()).unwrap();
14547            let result = std::panic::catch_unwind(|| super::point_label_overrides(&labels));
14548            assert!(result.is_ok(), "point-label parser panicked for {xml}");
14549            assert!(
14550                result.unwrap().is_err(),
14551                "malformed point labels parsed: {xml}"
14552            );
14553        }
14554
14555        let foreign = format!(
14556            r#"<q:dLbls xmlns:q="{C_NS}" xmlns:x="urn:foreign"><x:dLbl><x:idx val="0"/></x:dLbl></q:dLbls>"#
14557        );
14558        let labels = CT_DLbls::from_xml(foreign.as_bytes()).unwrap();
14559        assert!(super::point_label_overrides(&labels).unwrap().is_empty());
14560    }
14561
14562    #[test]
14563    fn percentage_label_aggregation_rejects_nonfinite_totals() {
14564        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14565        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14566        let Plot::Bar { series, .. } = plot else {
14567            panic!("expected bar plot");
14568        };
14569        series[0].values.values = vec![f64::MAX, f64::MAX];
14570        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14571        super::render_data_labels(
14572            plot,
14573            chart_bounds(),
14574            DispBlanksAs::Gap,
14575            super::GeometryOptions::default(),
14576            &mut fonts,
14577            &mut Vec::new(),
14578        )
14579        .expect("value-only labels do not need a percentage total");
14580        let Plot::Bar { data_labels, .. } = plot else {
14581            panic!("expected bar plot");
14582        };
14583        data_labels.as_mut().unwrap().show_percent = true;
14584        let error = super::render_data_labels(
14585            plot,
14586            chart_bounds(),
14587            DispBlanksAs::Gap,
14588            super::GeometryOptions::default(),
14589            &mut fonts,
14590            &mut Vec::new(),
14591        )
14592        .unwrap_err();
14593        assert!(error.to_string().contains("data-label percentage total"));
14594
14595        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>"#;
14596        let plot_xml = bar_plot("").replace(
14597            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14598            &format!(
14599                r#"<q:dLbls>{overrides}<q:showVal val="1"/><q:showPercent val="1"/></q:dLbls>"#
14600            ),
14601        );
14602        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14603        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14604        let Plot::Bar { series, .. } = plot else {
14605            panic!("expected bar plot");
14606        };
14607        series[0].values.values = vec![f64::MAX, f64::MAX];
14608        super::render_data_labels(
14609            plot,
14610            chart_bounds(),
14611            DispBlanksAs::Gap,
14612            super::GeometryOptions::default(),
14613            &mut fonts,
14614            &mut Vec::new(),
14615        )
14616        .expect("effective point overrides disable percentage aggregation");
14617    }
14618
14619    #[test]
14620    fn percentage_labels_use_effective_number_format_precision() {
14621        let mut chart = CT_ChartSpace::from_xml(chart_with_plot(&bar_plot("")).as_bytes()).unwrap();
14622        let plot = &mut chart.chart.plot_area.plots_mut().unwrap()[0];
14623        let Plot::Bar {
14624            series,
14625            data_labels,
14626            ..
14627        } = plot
14628        else {
14629            panic!("expected bar plot");
14630        };
14631        series[0].values.values = vec![1.0, 2.0];
14632        let labels = data_labels.as_mut().unwrap();
14633        labels.show_value = false;
14634        labels.show_percent = true;
14635        labels.number_format = Some(NumberFormat::new("0.00%".to_owned(), false).unwrap());
14636        let mut fonts = FontManager::new_deterministic().expect("deterministic fonts");
14637        let mut rendered = Vec::new();
14638        super::render_data_labels(
14639            plot,
14640            chart_bounds(),
14641            DispBlanksAs::Gap,
14642            super::GeometryOptions::default(),
14643            &mut fonts,
14644            &mut rendered,
14645        )
14646        .unwrap();
14647        let text: Vec<_> = rendered
14648            .iter()
14649            .filter_map(|element| match element {
14650                PositionedElement::Text(run) => Some(run.text.as_str()),
14651                _ => None,
14652            })
14653            .collect();
14654        assert_eq!(text, vec!["33.33%", "66.67%"]);
14655
14656        let point_override = r#"<q:dLbl><q:idx val="0"/><q:numFmt formatCode="0.00%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbl>"#;
14657        let plot_xml = bar_plot("").replace(
14658            r#"<q:dLbls><q:showVal val="1"/></q:dLbls>"#,
14659            &format!(
14660                r#"<q:dLbls>{point_override}<q:numFmt formatCode="0%" sourceLinked="0"/><q:showPercent val="1"/></q:dLbls>"#
14661            ),
14662        );
14663        let chart = CT_ChartSpace::from_xml(chart_with_plot(&plot_xml).as_bytes()).unwrap();
14664        let plot = &chart.chart.plot_area.plots().unwrap()[0];
14665        let mut rendered = Vec::new();
14666        super::render_data_labels(
14667            plot,
14668            chart_bounds(),
14669            DispBlanksAs::Gap,
14670            super::GeometryOptions::default(),
14671            &mut fonts,
14672            &mut rendered,
14673        )
14674        .unwrap();
14675        let text: Vec<_> = rendered
14676            .iter()
14677            .filter_map(|element| match element {
14678                PositionedElement::Text(run) => Some(run.text.as_str()),
14679                _ => None,
14680            })
14681            .collect();
14682        assert_eq!(text, vec!["33.33%", "67%"]);
14683    }
14684
14685    #[test]
14686    fn core_chart_shells_preserve_unmodelled_children_byte_for_byte() {
14687        let preserved = [
14688            br#"<x:root x:id="1"><x:nested>one &amp; two</x:nested><!--root--></x:root>"#
14689                .as_slice(),
14690            br#"<x:title x:id="2"><x:nested><![CDATA[title]]></x:nested></x:title>"#.as_slice(),
14691            br#"<x:plot x:id="3"><x:nested>plot</x:nested></x:plot>"#.as_slice(),
14692            br#"<x:legend x:id="4"><x:nested>legend</x:nested></x:legend>"#.as_slice(),
14693            br#"<x:extension x:id="5"><x:nested>extension</x:nested></x:extension>"#.as_slice(),
14694            br#"<!--space-comment-->"#.as_slice(),
14695            br#"<?space processing?>"#.as_slice(),
14696            br#"<!--chart-comment-->"#.as_slice(),
14697            br#"<?title processing?>"#.as_slice(),
14698            br#"<!--flag-comment-->"#.as_slice(),
14699            br#"<?flag processing?>"#.as_slice(),
14700            br#"<x:producerShape/>"#.as_slice(),
14701            br#"<x:producerText/>"#.as_slice(),
14702        ];
14703        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>"#;
14704        let parsed = CT_ChartSpace::from_xml(xml).unwrap();
14705        let written = parsed.to_xml().unwrap();
14706        for raw in preserved {
14707            assert!(
14708                written.windows(raw.len()).any(|window| window == raw),
14709                "preserved subtree changed: {}",
14710                String::from_utf8_lossy(raw)
14711            );
14712        }
14713        let written_text = String::from_utf8(written.clone()).unwrap();
14714        assert!(written_text.contains(r#"x:keep="one &amp; two""#));
14715        assert_eq!(parsed, CT_ChartSpace::from_xml(&written).unwrap());
14716    }
14717
14718    #[test]
14719    fn every_corpus_chart_part_round_trips_structurally() {
14720        let Some(corpus) = require_or_skip_corpus() else {
14721            return;
14722        };
14723        verify_fetched_corpus(&corpus);
14724        let mut chart_parts = 0usize;
14725        let mut decks_with_charts = HashSet::new();
14726        for path in manifest_paths() {
14727            let package = OpcPackage::open(corpus.join(path))
14728                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
14729            for (part, xml) in &package.parts {
14730                if !is_chart_part(part) {
14731                    continue;
14732                }
14733                let parsed = CT_ChartSpace::from_xml(xml)
14734                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
14735                let written = parsed
14736                    .to_xml()
14737                    .unwrap_or_else(|error| panic!("{path} {part}: write failed: {error}"));
14738                let original_preserved = preserved_chartml_regions(xml)
14739                    .unwrap_or_else(|error| panic!("{path} {part}: original scan failed: {error}"));
14740                let written_preserved = preserved_chartml_regions(&written)
14741                    .unwrap_or_else(|error| panic!("{path} {part}: written scan failed: {error}"));
14742                assert_eq!(
14743                    original_preserved.len(),
14744                    written_preserved.len(),
14745                    "{path} {part}: preserved ChartML region count changed"
14746                );
14747                for (index, (original, written)) in original_preserved
14748                    .iter()
14749                    .zip(&written_preserved)
14750                    .enumerate()
14751                {
14752                    assert_eq!(
14753                        original.parent_path, written.parent_path,
14754                        "{path} {part}: preserved region {index} parent changed"
14755                    );
14756                    assert_eq!(
14757                        original.boundary, written.boundary,
14758                        "{path} {part}: preserved region {index} boundary changed"
14759                    );
14760                    assert_eq!(
14761                        original.sibling_order, written.sibling_order,
14762                        "{path} {part}: preserved region {index} order changed"
14763                    );
14764                    assert_eq!(
14765                        original.xml, written.xml,
14766                        "{path} {part}: preserved region {index} bytes changed"
14767                    );
14768                }
14769                let reparsed = CT_ChartSpace::from_xml(&written)
14770                    .unwrap_or_else(|error| panic!("{path} {part}: written parse failed: {error}"));
14771                assert_eq!(parsed, reparsed, "{path} {part}: core model changed");
14772                chart_parts += 1;
14773                decks_with_charts.insert(path);
14774            }
14775        }
14776        assert!(
14777            chart_parts > 0,
14778            "the pinned corpus contained no chart parts"
14779        );
14780        eprintln!(
14781            "ChartML corpus gate checked {chart_parts} chart parts across {} decks",
14782            decks_with_charts.len()
14783        );
14784    }
14785
14786    #[derive(Debug, Eq, PartialEq)]
14787    struct PreservedChartRegion {
14788        parent_path: Vec<Vec<u8>>,
14789        boundary: usize,
14790        sibling_order: usize,
14791        xml: Vec<u8>,
14792    }
14793
14794    struct PreservationFrame {
14795        local_name: Vec<u8>,
14796        boundary: usize,
14797        next_sibling: usize,
14798    }
14799
14800    fn preserved_chartml_regions(xml: &[u8]) -> Result<Vec<PreservedChartRegion>, String> {
14801        let mut reader = Reader::from_reader(xml);
14802        let mut buffer = Vec::new();
14803        let mut parents: Vec<PreservationFrame> = Vec::new();
14804        let mut preserved = Vec::new();
14805        loop {
14806            match reader.read_event_into(&mut buffer) {
14807                Ok(Event::Start(element)) => {
14808                    let child = local_name(element.name().as_ref()).to_vec();
14809                    if parents.is_empty() && child == b"chartSpace" {
14810                        parents.push(PreservationFrame {
14811                            local_name: child,
14812                            boundary: 0,
14813                            next_sibling: 0,
14814                        });
14815                    } else if let Some(parent) = parents.last()
14816                        && is_opaque_chartml_child(&parent.local_name, &child)
14817                    {
14818                        let boundary =
14819                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
14820                        let xml = capture_element(&mut reader, &element)
14821                            .map_err(|error| error.to_string())?;
14822                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
14823                        advance_preservation_boundary(&mut parents, &child);
14824                    } else {
14825                        advance_preservation_boundary(&mut parents, &child);
14826                        if is_chartml_preservation_parent(&child) {
14827                            parents.push(PreservationFrame {
14828                                local_name: child,
14829                                boundary: 0,
14830                                next_sibling: 0,
14831                            });
14832                        } else {
14833                            capture_element(&mut reader, &element)
14834                                .map_err(|error| error.to_string())?;
14835                        }
14836                    }
14837                }
14838                Ok(Event::Empty(element)) => {
14839                    let name = element.name();
14840                    let child = local_name(name.as_ref()).to_vec();
14841                    if let Some(parent) = parents.last()
14842                        && is_opaque_chartml_child(&parent.local_name, &child)
14843                    {
14844                        let boundary =
14845                            preserved_child_boundary(&parent.local_name, &child, parent.boundary);
14846                        let xml =
14847                            capture_empty_element(&element).map_err(|error| error.to_string())?;
14848                        record_preserved_region(&mut parents, &mut preserved, boundary, xml);
14849                    }
14850                    advance_preservation_boundary(&mut parents, &child);
14851                }
14852                Ok(
14853                    event @ (Event::Comment(_)
14854                    | Event::PI(_)
14855                    | Event::CData(_)
14856                    | Event::GeneralRef(_)),
14857                ) if !parents.is_empty() => {
14858                    let boundary = parents.last().expect("preservation parent").boundary;
14859                    let xml = capture_event(event).map_err(|error| error.to_string())?;
14860                    record_preserved_region(&mut parents, &mut preserved, boundary, xml);
14861                }
14862                Ok(Event::End(_)) => {
14863                    parents.pop();
14864                }
14865                Ok(Event::Eof) => break,
14866                Ok(_) => {}
14867                Err(error) => return Err(error.to_string()),
14868            }
14869            buffer.clear();
14870        }
14871        Ok(preserved)
14872    }
14873
14874    fn record_preserved_region(
14875        parents: &mut [PreservationFrame],
14876        preserved: &mut Vec<PreservedChartRegion>,
14877        boundary: usize,
14878        xml: Vec<u8>,
14879    ) {
14880        let sibling_order = parents.last().expect("preservation parent").next_sibling;
14881        parents
14882            .last_mut()
14883            .expect("preservation parent")
14884            .next_sibling += 1;
14885        preserved.push(PreservedChartRegion {
14886            parent_path: parents
14887                .iter()
14888                .map(|parent| parent.local_name.clone())
14889                .collect(),
14890            boundary,
14891            sibling_order,
14892            xml,
14893        });
14894    }
14895
14896    fn advance_preservation_boundary(parents: &mut [PreservationFrame], child: &[u8]) {
14897        let Some(parent) = parents.last_mut() else {
14898            return;
14899        };
14900        parent.boundary = parent
14901            .boundary
14902            .max(preserved_boundary_after(&parent.local_name, child));
14903    }
14904
14905    fn preserved_child_boundary(parent: &[u8], child: &[u8], current: usize) -> usize {
14906        match parent {
14907            b"chartSpace" => match child {
14908                b"date1904" => 0,
14909                b"lang" => 1,
14910                b"roundedCorners" => 2,
14911                b"style" => 3,
14912                b"clrMapOvr" => 4,
14913                b"pivotSource" => 5,
14914                b"protection" => 6,
14915                b"externalData" => 10,
14916                b"printSettings" => 11,
14917                b"userShapes" => 12,
14918                b"extLst" => 13,
14919                _ => current,
14920            },
14921            b"chart" => match child {
14922                b"pivotFmts" => 2,
14923                b"view3D" => 3,
14924                b"floor" => 4,
14925                b"sideWall" => 5,
14926                b"backWall" => 6,
14927                b"showDLblsOverMax" | b"extLst" => 11,
14928                _ => current,
14929            },
14930            _ => current,
14931        }
14932    }
14933
14934    fn preserved_boundary_after(parent: &[u8], child: &[u8]) -> usize {
14935        match parent {
14936            b"chartSpace" => match child {
14937                b"date1904" => 1,
14938                b"lang" => 2,
14939                b"roundedCorners" => 3,
14940                b"style" => 4,
14941                b"clrMapOvr" => 5,
14942                b"pivotSource" => 6,
14943                b"protection" => 7,
14944                b"chart" => 8,
14945                b"spPr" => 9,
14946                b"txPr" => 10,
14947                b"externalData" => 11,
14948                b"printSettings" => 12,
14949                b"userShapes" => 13,
14950                b"extLst" => 14,
14951                _ => 0,
14952            },
14953            b"chart" => match child {
14954                b"title" => 1,
14955                b"autoTitleDeleted" => 2,
14956                b"pivotFmts" => 3,
14957                b"view3D" => 4,
14958                b"floor" => 5,
14959                b"sideWall" => 6,
14960                b"backWall" => 7,
14961                b"plotArea" => 8,
14962                b"legend" => 9,
14963                b"plotVisOnly" => 10,
14964                b"dispBlanksAs" => 11,
14965                b"showDLblsOverMax" | b"extLst" => 11,
14966                _ => 0,
14967            },
14968            b"pieChart" => match child {
14969                b"ser" => 1,
14970                b"dLbls" => 2,
14971                b"firstSliceAng" => 3,
14972                _ => 0,
14973            },
14974            b"doughnutChart" => match child {
14975                b"ser" => 1,
14976                b"dLbls" => 2,
14977                b"firstSliceAng" => 3,
14978                b"holeSize" => 4,
14979                _ => 0,
14980            },
14981            b"areaChart" => match child {
14982                b"grouping" => 1,
14983                b"ser" => 2,
14984                b"dLbls" => 3,
14985                b"axId" => 4,
14986                _ => 0,
14987            },
14988            b"scatterChart" | b"radarChart" => match child {
14989                b"scatterStyle" | b"radarStyle" => 1,
14990                b"ser" => 2,
14991                b"dLbls" => 3,
14992                b"axId" => 4,
14993                _ => 0,
14994            },
14995            _ => 0,
14996        }
14997    }
14998
14999    fn is_opaque_chartml_child(parent: &[u8], child: &[u8]) -> bool {
15000        match parent {
15001            b"chartSpace" => !matches!(child, b"chart" | b"spPr" | b"txPr"),
15002            b"chart" => !matches!(
15003                child,
15004                b"title"
15005                    | b"autoTitleDeleted"
15006                    | b"plotArea"
15007                    | b"legend"
15008                    | b"plotVisOnly"
15009                    | b"dispBlanksAs"
15010            ),
15011            b"plotArea" => !matches!(
15012                child,
15013                b"barChart"
15014                    | b"lineChart"
15015                    | b"pieChart"
15016                    | b"doughnutChart"
15017                    | b"areaChart"
15018                    | b"scatterChart"
15019                    | b"radarChart"
15020                    | b"catAx"
15021                    | b"valAx"
15022                    | b"dateAx"
15023                    | b"serAx"
15024            ),
15025            b"pieChart" => !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng"),
15026            b"doughnutChart" => {
15027                !matches!(child, b"ser" | b"dLbls" | b"firstSliceAng" | b"holeSize")
15028            }
15029            b"areaChart" => !matches!(child, b"grouping" | b"ser" | b"dLbls" | b"axId"),
15030            b"scatterChart" => !matches!(child, b"scatterStyle" | b"ser" | b"dLbls" | b"axId"),
15031            b"radarChart" => !matches!(child, b"radarStyle" | b"ser" | b"dLbls" | b"axId"),
15032            b"title" | b"legend" => true,
15033            _ => false,
15034        }
15035    }
15036
15037    fn is_chartml_preservation_parent(parent: &[u8]) -> bool {
15038        matches!(
15039            parent,
15040            b"chartSpace"
15041                | b"chart"
15042                | b"title"
15043                | b"plotArea"
15044                | b"pieChart"
15045                | b"doughnutChart"
15046                | b"areaChart"
15047                | b"scatterChart"
15048                | b"radarChart"
15049                | b"legend"
15050                | b"autoTitleDeleted"
15051                | b"plotVisOnly"
15052                | b"dispBlanksAs"
15053        )
15054    }
15055
15056    #[test]
15057    fn rpptx_chart_is_an_explicit_publication_candidate() {
15058        let manifest = include_str!("../Cargo.toml");
15059        let workspace = include_str!("../../../Cargo.toml");
15060        assert!(manifest.contains("name = \"rpptx-chart\""));
15061        assert!(manifest.contains("version = \"0.1.3\""));
15062        assert!(manifest.contains("publish = true"));
15063        for dependency in [
15064            "oxml-core.workspace",
15065            "oxml-drawing.workspace",
15066            "quick-xml.workspace",
15067        ] {
15068            assert!(manifest.contains(dependency));
15069        }
15070        assert!(!manifest.contains("rpptx.workspace"));
15071        assert!(!manifest.contains("rdocx.workspace"));
15072        assert!(workspace.contains("\"crates/rpptx-chart\""));
15073        assert!(
15074            workspace
15075                .contains("rpptx-chart = { path = \"crates/rpptx-chart\", version = \"0.1.3\" }")
15076        );
15077    }
15078
15079    fn first_series_xml(xml: &[u8]) -> Option<Vec<u8>> {
15080        let mut reader = Reader::from_reader(xml);
15081        let mut buffer = Vec::new();
15082        loop {
15083            match reader.read_event_into(&mut buffer).ok()? {
15084                Event::Start(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15085                    return capture_element(&mut reader, &element).ok();
15086                }
15087                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"ser") => {
15088                    return capture_empty_element(&element).ok();
15089                }
15090                Event::Eof => return None,
15091                _ => {}
15092            }
15093            buffer.clear();
15094        }
15095    }
15096
15097    fn replace_once(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
15098        let offset = haystack
15099            .windows(needle.len())
15100            .position(|window| window == needle)
15101            .expect("serialized source series occurs in chart part");
15102        assert!(
15103            !haystack[offset + needle.len()..]
15104                .windows(needle.len())
15105                .any(|window| window == needle),
15106            "source series unexpectedly occurs more than once"
15107        );
15108        let mut result = Vec::with_capacity(haystack.len() - needle.len() + replacement.len());
15109        result.extend_from_slice(&haystack[..offset]);
15110        result.extend_from_slice(replacement);
15111        result.extend_from_slice(&haystack[offset + needle.len()..]);
15112        result
15113    }
15114
15115    fn assert_command_version(program: &str, arguments: &[&str], expected: &str) {
15116        let output = Command::new(program)
15117            .args(arguments)
15118            .output()
15119            .unwrap_or_else(|error| panic!("run {program} version check: {error}"));
15120        assert!(
15121            output.status.success(),
15122            "{program} version check failed: {}",
15123            String::from_utf8_lossy(&output.stderr)
15124        );
15125        let stdout = String::from_utf8(output.stdout).expect("version stdout is UTF-8");
15126        let stderr = String::from_utf8(output.stderr).expect("version stderr is UTF-8");
15127        let actual = if stdout.trim().is_empty() {
15128            stderr.trim()
15129        } else {
15130            stdout.trim()
15131        };
15132        assert_eq!(
15133            actual.lines().next(),
15134            Some(expected),
15135            "{program} version drift"
15136        );
15137    }
15138
15139    fn sha256(path: &Path) -> String {
15140        let output = Command::new("shasum")
15141            .args(["-a", "256"])
15142            .arg(path)
15143            .output()
15144            .unwrap_or_else(|error| panic!("{}: run shasum: {error}", path.display()));
15145        assert!(
15146            output.status.success(),
15147            "{}: shasum failed: {}",
15148            path.display(),
15149            String::from_utf8_lossy(&output.stderr)
15150        );
15151        String::from_utf8(output.stdout)
15152            .expect("shasum output is UTF-8")
15153            .split_whitespace()
15154            .next()
15155            .expect("shasum digest")
15156            .to_owned()
15157    }
15158
15159    fn workspace_root() -> PathBuf {
15160        Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
15161    }
15162
15163    fn corpus_dir() -> PathBuf {
15164        std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
15165            .map(PathBuf::from)
15166            .unwrap_or_else(|| workspace_root().join("corpus/pptx"))
15167    }
15168
15169    fn require_or_skip_corpus() -> Option<PathBuf> {
15170        let corpus = corpus_dir();
15171        if corpus.is_dir() {
15172            return Some(corpus);
15173        }
15174        assert_ne!(
15175            std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
15176            Some(std::ffi::OsStr::new("1")),
15177            "the pinned corpus is required but {} does not exist",
15178            corpus.display()
15179        );
15180        eprintln!(
15181            "ChartML corpus gate skipped because {} is absent",
15182            corpus.display()
15183        );
15184        None
15185    }
15186
15187    fn verify_fetched_corpus(corpus: &Path) {
15188        let status = Command::new("python3")
15189            .arg(workspace_root().join("scripts/fetch_pptx_corpus.py"))
15190            .arg("--check")
15191            .env("RDOCX_PPTX_CORPUS_DIR", corpus)
15192            .status()
15193            .expect("run corpus verifier");
15194        assert!(status.success(), "pinned corpus verification failed");
15195    }
15196
15197    fn manifest_paths() -> Vec<&'static str> {
15198        let mut lines = MANIFEST.lines();
15199        assert_eq!(lines.next(), Some("path\tproducer\tsha256\turl"));
15200        let paths: Vec<_> = lines
15201            .map(|line| line.split('\t').next().expect("manifest path"))
15202            .collect();
15203        assert_eq!(paths.len(), EXPECTED_DECKS);
15204        paths
15205    }
15206
15207    fn is_chart_part(part: &str) -> bool {
15208        let Some(stem) = part
15209            .strip_prefix("/ppt/charts/chart")
15210            .and_then(|name| name.strip_suffix(".xml"))
15211        else {
15212            return false;
15213        };
15214        !stem.is_empty() && stem.bytes().all(|byte| byte.is_ascii_digit())
15215    }
15216
15217    fn plot_series(index: u32) -> String {
15218        format!(
15219            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>"#
15220        )
15221    }
15222
15223    fn sparse_category_series() -> &'static str {
15224        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>"#
15225    }
15226
15227    fn sparse_scatter_series() -> &'static str {
15228        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>"#
15229    }
15230
15231    fn chart_with_plot(plot: &str) -> String {
15232        format!(
15233            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>"#
15234        )
15235    }
15236
15237    fn chart_with_optional_axes(plot: &str, axes: bool) -> String {
15238        let axis_xml = if axes {
15239            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>"#
15240        } else {
15241            ""
15242        };
15243        format!(
15244            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>"#
15245        )
15246    }
15247
15248    fn numeric_plot_series(index: u32) -> String {
15249        format!(
15250            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>"#
15251        )
15252    }
15253
15254    fn scatter_without_x(index: u32) -> String {
15255        format!(
15256            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>"#
15257        )
15258    }
15259
15260    fn scatter_without_y(index: u32) -> String {
15261        format!(
15262            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>"#
15263        )
15264    }
15265
15266    fn remaining_plot_fixtures() -> Vec<(&'static str, String, bool)> {
15267        let series = plot_series(0);
15268        let scatter = numeric_plot_series(0);
15269        vec![
15270            (
15271                "pieChart",
15272                format!(
15273                    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>"#
15274                ),
15275                false,
15276            ),
15277            (
15278                "doughnutChart",
15279                format!(
15280                    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>"#
15281                ),
15282                false,
15283            ),
15284            (
15285                "areaChart",
15286                format!(
15287                    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>"#
15288                ),
15289                true,
15290            ),
15291            (
15292                "scatterChart",
15293                format!(
15294                    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>"#
15295                ),
15296                true,
15297            ),
15298            (
15299                "radarChart",
15300                format!(
15301                    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>"#
15302                ),
15303                true,
15304            ),
15305        ]
15306    }
15307
15308    fn bar_plot(extra: &str) -> String {
15309        format!(
15310            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>"#,
15311            plot_series(0)
15312        )
15313    }
15314
15315    fn line_plot(extra: &str) -> String {
15316        format!(
15317            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>"#,
15318            plot_series(0)
15319        )
15320    }
15321
15322    #[test]
15323    fn bar_and_line_plots_round_trip_and_render() {
15324        for (xml, is_bar) in [
15325            (chart_with_plot(&bar_plot("")), true),
15326            (chart_with_plot(&line_plot("")), false),
15327        ] {
15328            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15329            let plots = parsed.chart.plot_area.plots().unwrap();
15330            assert_eq!(plots.len(), 1);
15331            assert_eq!(matches!(plots[0], Plot::Bar { .. }), is_bar);
15332            assert_eq!(matches!(plots[0], Plot::Line { .. }), !is_bar);
15333            assert_eq!(parsed.chart.plot_area.axes().unwrap().len(), 2);
15334            let written = parsed.to_xml().unwrap();
15335            let reparsed = CT_ChartSpace::from_xml(&written).unwrap();
15336            assert_eq!(parsed, reparsed);
15337        }
15338        if let Some(corpus) = require_or_skip_corpus() {
15339            verify_bar_and_line_viewer_gate(&corpus);
15340        }
15341    }
15342
15343    #[test]
15344    fn ppm_parser_preserves_whitespace_valued_first_pixels() {
15345        for first_pixel in *b" \n\r\t" {
15346            let mut ppm = b"P6\n1 1\n255\n".to_vec();
15347            ppm.extend_from_slice(&[first_pixel, 0x7f, 0xff]);
15348            let (width, height, pixels) = ppm_pixels(&ppm);
15349            assert_eq!((width, height), (1, 1));
15350            assert_eq!(pixels, [first_pixel, 0x7f, 0xff]);
15351        }
15352
15353        let ppm = b"P6\r\n1 1\r\n255\r\n\x7f\xff";
15354        assert_eq!(ppm_pixels(ppm).2, [b'\n', 0x7f, 0xff]);
15355    }
15356
15357    #[test]
15358    fn bar_and_line_plots_write_fixed_prefixes_in_schema_order() {
15359        for xml in [
15360            chart_with_plot(&bar_plot(r#"<q:extLst><x:bar-tail/></q:extLst>"#)),
15361            chart_with_plot(&line_plot(r#"<q:extLst><x:line-tail/></q:extLst>"#)),
15362        ] {
15363            let parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15364            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15365            assert!(written.contains("<c:barChart") || written.contains("<c:lineChart"));
15366            assert!(!written.contains("<q:barChart"));
15367            assert!(!written.contains("<q:lineChart"));
15368            if written.contains("<c:barChart") {
15369                let positions: Vec<_> = [
15370                    "<c:barDir",
15371                    "<c:grouping",
15372                    "<q:varyColors",
15373                    "<c:ser",
15374                    "<c:dLbls",
15375                    "<c:gapWidth",
15376                    "<c:overlap",
15377                    "<q:serLines",
15378                    "<c:axId",
15379                    "<q:extLst",
15380                ]
15381                .iter()
15382                .map(|tag| written.find(tag).unwrap())
15383                .collect();
15384                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15385            } else {
15386                let positions: Vec<_> = [
15387                    "<c:grouping",
15388                    "<q:varyColors",
15389                    "<c:ser",
15390                    "<c:dLbls",
15391                    "<q:dropLines",
15392                    "<c:marker",
15393                    "<c:smooth",
15394                    "<c:axId",
15395                    "<q:extLst",
15396                ]
15397                .iter()
15398                .map(|tag| written.find(tag).unwrap())
15399                .collect();
15400                assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
15401            }
15402            assert!(CT_ChartSpace::from_xml(written.as_bytes()).is_ok());
15403        }
15404
15405        for plot in [bar_plot(""), line_plot("")] {
15406            let first = plot_series(0);
15407            let with_series_boundary = plot.replacen(
15408                &first,
15409                &format!(
15410                    "{first}<!--between-series--><?series-boundary?>\n  {}",
15411                    plot_series(1)
15412                ),
15413                1,
15414            );
15415            let with_axis_boundary = with_series_boundary.replacen(
15416                r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15417                r#"<q:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15418  <q:axId val="-1884097184"/>"#,
15419                1,
15420            );
15421            let parsed =
15422                CT_ChartSpace::from_xml(chart_with_plot(&with_axis_boundary).as_bytes()).unwrap();
15423            let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15424            assert!(written.contains("</c:ser><!--between-series--><?series-boundary?>\n  <c:ser"));
15425            assert!(written.contains(
15426                r#"<c:axId val="-1884094432"/><!--between-axis-ids--><?axis-boundary?>
15427  <c:axId val="-1884097184"/>"#
15428            ));
15429            assert_eq!(parsed, CT_ChartSpace::from_xml(written.as_bytes()).unwrap());
15430        }
15431    }
15432
15433    #[test]
15434    fn malformed_bar_and_line_plots_return_errors_without_panicking() {
15435        let valid_series = plot_series(0);
15436        let cases = [
15437            format!(r#"<q:barChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15438            format!(r#"<q:barChart><q:barDir val="diagonal"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15439            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>"#),
15440            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>"#),
15441            format!(r#"<q:lineChart><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15442            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:marker val="maybe"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#),
15443            r#"<q:lineChart><q:grouping val="standard"/><q:axId val="1"/><q:axId val="2"/></q:lineChart>"#.to_owned(),
15444            format!(r#"<q:lineChart><q:grouping val="standard"/>{valid_series}<q:axId val="1"/><q:axId val="1"/></q:lineChart>"#),
15445            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>"#),
15446            format!(r#"<q:barChart><q:barDir val="col"/><q:grouping val="clustered"/>{valid_series}<q:axId val="1"/><q:axId val="2"/></q:barChart>"#),
15447        ];
15448        for plot in cases {
15449            let xml = chart_with_plot(&plot);
15450            let result = std::panic::catch_unwind(|| CT_ChartSpace::from_xml(xml.as_bytes()));
15451            assert!(result.is_ok(), "plot parser panicked for {plot}");
15452            assert!(result.unwrap().is_err(), "malformed plot parsed: {plot}");
15453        }
15454
15455        let missing_axis = format!(
15456            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>"#,
15457            bar_plot("")
15458        );
15459        assert!(CT_ChartSpace::from_xml(missing_axis.as_bytes()).is_err());
15460
15461        let values =
15462            NumericData::new("S!$A$1".to_owned(), "General".to_owned(), vec![1.0]).unwrap();
15463        let series = vec![Series::new(0, 0, values)];
15464        let ids = [AxisId::new(1).unwrap(), AxisId::new(2).unwrap()];
15465        let mut invalid =
15466            Plot::bar(BarDirection::Column, BarGrouping::Clustered, series, ids).unwrap();
15467        if let Plot::Bar { gap_width, .. } = &mut invalid {
15468            *gap_width = 501;
15469        }
15470        assert!(invalid.validate().is_err());
15471    }
15472
15473    #[test]
15474    fn unsupported_and_combo_plots_remain_byte_preserved() {
15475        let bar = bar_plot("");
15476        let line = line_plot("");
15477        let combo = chart_with_plot(&format!("<!--before-->{bar}<?between?>{line}<!--after-->"));
15478        let parsed = CT_ChartSpace::from_xml(combo.as_bytes()).unwrap();
15479        assert!(parsed.chart.plot_area.plots().is_err());
15480        let written = parsed.to_xml().unwrap();
15481        for raw in [
15482            bar.as_bytes(),
15483            line.as_bytes(),
15484            br#"<!--before-->"#.as_slice(),
15485            br#"<?between?>"#.as_slice(),
15486            br#"<!--after-->"#.as_slice(),
15487        ] {
15488            assert!(written.windows(raw.len()).any(|window| window == raw));
15489        }
15490
15491        let unsupported = chart_with_plot(
15492            r#"<q:bar3DChart x:keep="three"><q:barDir val="col"/><q:ser><x:opaque/></q:ser></q:bar3DChart>"#,
15493        );
15494        let parsed = CT_ChartSpace::from_xml(unsupported.as_bytes()).unwrap();
15495        assert!(parsed.chart.plot_area.plots().is_err());
15496        assert!(
15497            String::from_utf8(parsed.to_xml().unwrap())
15498                .unwrap()
15499                .contains(r#"<q:bar3DChart x:keep="three">"#)
15500        );
15501    }
15502
15503    #[test]
15504    fn public_plot_edits_preserve_axes_and_unselected_payloads() {
15505        let xml = chart_with_plot(&bar_plot(
15506            r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#,
15507        ));
15508        let mut parsed = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15509        let axes_before = parsed.chart.plot_area.axes().unwrap();
15510        match &mut parsed.chart.plot_area.plots_mut().unwrap()[0] {
15511            Plot::Bar {
15512                gap_width,
15513                overlap,
15514                series,
15515                ..
15516            } => {
15517                *gap_width = 225;
15518                *overlap = -25;
15519                series[0].index = 7;
15520                series[0].values.values[0] = 9.0;
15521            }
15522            _ => panic!("expected bar plot"),
15523        }
15524        let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
15525        assert!(written.contains(r#"<c:gapWidth val="225"/>"#));
15526        assert!(written.contains(r#"<c:overlap val="-25"/>"#));
15527        assert!(written.contains("<c:v>9</c:v>"));
15528        assert!(written.contains("<q:varyColors val=\"1\"/>"));
15529        assert!(written.find("<q:varyColors").unwrap() < written.find("<c:ser").unwrap());
15530        assert!(written.contains("<q:serLines><x:line/></q:serLines>"));
15531        assert!(written.contains(r#"<q:extLst><q:ext uri="keep"><x:tail/></q:ext></q:extLst>"#));
15532        let reparsed = CT_ChartSpace::from_xml(written.as_bytes()).unwrap();
15533        assert_eq!(axes_before, reparsed.chart.plot_area.axes().unwrap());
15534
15535        let first = plot_series(0);
15536        let colliding_series =
15537            bar_plot("").replacen(&first, &format!("{first}{}", plot_series(1)), 1);
15538        let mut collision =
15539            CT_ChartSpace::from_xml(chart_with_plot(&colliding_series).as_bytes()).unwrap();
15540        if let Plot::Bar { series, .. } = &mut collision.chart.plot_area.plots_mut().unwrap()[0] {
15541            series[0].index = series[1].index;
15542            series[0].order = series[1].order;
15543        }
15544        let collision = String::from_utf8(collision.to_xml().unwrap()).unwrap();
15545        assert!(collision.find("<q:varyColors").unwrap() < collision.find("<c:ser").unwrap());
15546
15547        let mut inserted = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15548        if let Plot::Bar { series, .. } = &mut inserted.chart.plot_area.plots_mut().unwrap()[0] {
15549            let mut new_series = series[0].clone();
15550            new_series.index = 99;
15551            new_series.order = 99;
15552            series.insert(0, new_series);
15553        }
15554        let inserted = String::from_utf8(inserted.to_xml().unwrap()).unwrap();
15555        assert!(inserted.find("<q:varyColors").unwrap() < inserted.find("<c:ser").unwrap());
15556
15557        let axis_payload = bar_plot("").replacen(
15558            r#"<q:axId val="-1884094432"/><q:axId val="-1884097184"/>"#,
15559            r#"<q:axId val="-1884094432" x:slot="category"/><!--axis-anchor--><q:axId val="-1884097184" x:slot="value"/>"#,
15560            1,
15561        );
15562        let mut swapped =
15563            CT_ChartSpace::from_xml(chart_with_plot(&axis_payload).as_bytes()).unwrap();
15564        if let Plot::Bar { axis_ids, .. } = &mut swapped.chart.plot_area.plots_mut().unwrap()[0] {
15565            axis_ids.swap(0, 1);
15566        }
15567        let swapped = String::from_utf8(swapped.to_xml().unwrap()).unwrap();
15568        let anchor = swapped.find("<!--axis-anchor-->").unwrap();
15569        let value_axis = swapped
15570            .find(r#"<c:axId val="-1884097184" x:slot="value"/>"#)
15571            .unwrap();
15572        let category_axis = swapped
15573            .find(r#"<c:axId val="-1884094432" x:slot="category"/>"#)
15574            .unwrap();
15575        let series_lines = swapped.find("<q:serLines").unwrap();
15576        assert!(series_lines < value_axis && series_lines < category_axis);
15577        assert!(anchor < value_axis && value_axis < category_axis);
15578
15579        let mut replaced = CT_ChartSpace::from_xml(xml.as_bytes()).unwrap();
15580        let Plot::Bar {
15581            series, axis_ids, ..
15582        } = &replaced.chart.plot_area.plots().unwrap()[0]
15583        else {
15584            panic!("expected bar plot");
15585        };
15586        let line = Plot::line(Grouping::Standard, series.clone(), *axis_ids).unwrap();
15587        replaced.chart.plot_area.plots_mut().unwrap()[0] = line;
15588        assert!(replaced.to_xml().is_err());
15589    }
15590
15591    #[test]
15592    fn every_corpus_bar_and_line_plot_round_trips_structurally() {
15593        let Some(corpus) = require_or_skip_corpus() else {
15594            return;
15595        };
15596        verify_fetched_corpus(&corpus);
15597        let mut typed_bar_count = 0usize;
15598        let mut typed_line_count = 0usize;
15599        let mut opaque_bar_count = 0usize;
15600        let mut opaque_line_count = 0usize;
15601        let mut opaque_combo_count = 0usize;
15602        for path in manifest_paths() {
15603            let package = OpcPackage::open(corpus.join(path))
15604                .unwrap_or_else(|error| panic!("{path}: open failed: {error}"));
15605            for (part, xml) in &package.parts {
15606                if !is_chart_part(part) {
15607                    continue;
15608                }
15609                let chart = CT_ChartSpace::from_xml(xml)
15610                    .unwrap_or_else(|error| panic!("{path} {part}: parse failed: {error}"));
15611                match chart.chart.plot_area.plots() {
15612                    Ok(plots) => {
15613                        for plot in plots {
15614                            match plot {
15615                                Plot::Bar { .. } => typed_bar_count += 1,
15616                                Plot::Line { .. } => typed_line_count += 1,
15617                                _ => {}
15618                            }
15619                        }
15620                    }
15621                    Err(_) => {
15622                        let mut preserved_plot_count = 0usize;
15623                        for raw in chart.chart.plot_area.raw_children.at(0) {
15624                            if let Ok(Some(local)) = super::chart_root_local(
15625                                raw,
15626                                &chart.chart.plot_area.namespace_bindings,
15627                            ) {
15628                                match local.as_slice() {
15629                                    b"barChart" => {
15630                                        opaque_bar_count += 1;
15631                                        preserved_plot_count += 1;
15632                                    }
15633                                    b"lineChart" => {
15634                                        opaque_line_count += 1;
15635                                        preserved_plot_count += 1;
15636                                    }
15637                                    _ => {}
15638                                }
15639                            }
15640                        }
15641                        if preserved_plot_count > 1 {
15642                            opaque_combo_count += 1;
15643                        }
15644                    }
15645                }
15646                let written = chart
15647                    .to_xml()
15648                    .unwrap_or_else(|error| panic!("{path} {part}: chart write failed: {error}"));
15649                let reparsed = CT_ChartSpace::from_xml(&written).unwrap_or_else(|error| {
15650                    panic!("{path} {part}: written chart parse failed: {error}")
15651                });
15652                assert_eq!(chart, reparsed, "{path} {part}: chart model changed");
15653            }
15654        }
15655        assert_eq!(typed_bar_count, 11, "typed corpus bar coverage changed");
15656        assert_eq!(typed_line_count, 2, "typed corpus line coverage changed");
15657        assert_eq!(opaque_bar_count, 1, "preserved combo bar coverage changed");
15658        assert_eq!(
15659            opaque_line_count, 1,
15660            "preserved combo line coverage changed"
15661        );
15662        assert_eq!(opaque_combo_count, 1, "preserved combo coverage changed");
15663        assert_eq!(typed_bar_count + opaque_bar_count, 12);
15664        assert_eq!(typed_line_count + opaque_line_count, 3);
15665        eprintln!(
15666            "ChartML plot corpus gate checked {typed_bar_count} typed bar, {typed_line_count} typed line, and one preserved bar-line combination"
15667        );
15668    }
15669
15670    fn verify_remaining_plot_viewer_gate(corpus: &Path) {
15671        verify_fetched_corpus(corpus);
15672        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15673        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15674        let source = corpus.join("bar-chart.pptx");
15675        let source_sha = sha256(&source);
15676        let temp_root = std::env::temp_dir().join(format!(
15677            "rpptx-chart-f122-viewer-gate-{}",
15678            std::process::id()
15679        ));
15680        if temp_root.exists() {
15681            fs::remove_dir_all(&temp_root).expect("remove stale F-122 evidence");
15682        }
15683        fs::create_dir_all(&temp_root).expect("create F-122 evidence root");
15684
15685        for (kind, plot, axes) in remaining_plot_fixtures() {
15686            let mut package = OpcPackage::open(&source).expect("open F-122 viewer source");
15687            package.set_part(
15688                "/ppt/charts/chart1.xml",
15689                CT_ChartSpace::from_xml(chart_with_optional_axes(&plot, axes).as_bytes())
15690                    .expect("parse F-122 viewer chart")
15691                    .to_xml()
15692                    .expect("serialize F-122 viewer chart"),
15693            );
15694            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15695            package.save(&unbound).expect("save F-122 viewer candidate");
15696            let candidate_sha = sha256(&unbound);
15697            let evidence = temp_root.join(format!("{source_sha}-{candidate_sha}"));
15698            fs::create_dir(&evidence).expect("create SHA-bound F-122 evidence directory");
15699            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15700            fs::rename(&unbound, &candidate).expect("bind F-122 candidate to SHA");
15701            assert_eq!(sha256(&candidate), candidate_sha);
15702            let render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15703            let bytes = fs::read(&render).expect("read F-122 viewer PPM");
15704            let (width, height, pixels) = ppm_pixels(&bytes);
15705            let left = width / 5;
15706            let right = width * 4 / 5;
15707            let top = height * 3 / 20;
15708            let bottom = height * 17 / 20;
15709            let mut nonblank = 0usize;
15710            for y in top..bottom {
15711                for x in left..right {
15712                    let offset = (y * width + x) * 3;
15713                    if pixels[offset..offset + 3]
15714                        .iter()
15715                        .any(|channel| *channel < 245)
15716                    {
15717                        nonblank += 1;
15718                    }
15719                }
15720            }
15721            const CHART_RECTANGLE_NONBLANK_THRESHOLD: usize = 1_000;
15722            assert!(
15723                nonblank >= CHART_RECTANGLE_NONBLANK_THRESHOLD,
15724                "{kind}: chart rectangle [{left},{top}) to [{right},{bottom}) contains {nonblank} nonblank pixels, below {CHART_RECTANGLE_NONBLANK_THRESHOLD}"
15725            );
15726            eprintln!(
15727                "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}",
15728                sha256(&render)
15729            );
15730        }
15731        fs::remove_dir_all(&temp_root).expect("remove F-122 viewer evidence");
15732    }
15733
15734    fn verify_bar_and_line_viewer_gate(corpus: &Path) {
15735        verify_fetched_corpus(corpus);
15736        assert_command_version("soffice", &["--version"], LIBREOFFICE_VERSION);
15737        assert_command_version("pdftoppm", &["-v"], PDFTOPPM_VERSION);
15738        let temp_root = std::env::temp_dir().join(format!(
15739            "rpptx-chart-f121-viewer-gate-{}",
15740            std::process::id()
15741        ));
15742        if temp_root.exists() {
15743            fs::remove_dir_all(&temp_root).expect("remove stale F-121 evidence");
15744        }
15745        fs::create_dir_all(&temp_root).expect("create F-121 evidence root");
15746
15747        for (kind, deck) in [("bar", "bar-chart.pptx"), ("line", "line-chart.pptx")] {
15748            let source = corpus.join(deck);
15749            let original_sha = sha256(&source);
15750            let mut package = OpcPackage::open(&source)
15751                .unwrap_or_else(|error| panic!("{deck}: open viewer source: {error}"));
15752            let chart_part = "/ppt/charts/chart1.xml";
15753            let original_parts = package.parts.clone();
15754            let parsed = CT_ChartSpace::from_xml(
15755                package
15756                    .get_part(chart_part)
15757                    .unwrap_or_else(|| panic!("{deck}: missing {chart_part}")),
15758            )
15759            .unwrap_or_else(|error| panic!("{deck}: parse viewer chart: {error}"));
15760            assert!(
15761                parsed.chart.plot_area.plots().is_ok(),
15762                "{deck}: plot is opaque"
15763            );
15764            package.set_part(
15765                chart_part,
15766                parsed
15767                    .to_xml()
15768                    .unwrap_or_else(|error| panic!("{deck}: serialize viewer chart: {error}")),
15769            );
15770            for (part, bytes) in &original_parts {
15771                if part == chart_part {
15772                    assert_ne!(package.parts[part], *bytes, "{deck}: chart did not rewrite");
15773                } else {
15774                    assert_eq!(package.parts[part], *bytes, "{deck}: changed part {part}");
15775                }
15776            }
15777
15778            let unbound = temp_root.join(format!("{kind}-candidate.pptx"));
15779            package
15780                .save(&unbound)
15781                .unwrap_or_else(|error| panic!("{deck}: save viewer candidate: {error}"));
15782            let candidate_sha = sha256(&unbound);
15783            let evidence = temp_root.join(format!("{original_sha}-{candidate_sha}"));
15784            fs::create_dir(&evidence).expect("create SHA-bound F-121 evidence directory");
15785            let original = evidence.join(format!("{kind}-original.pptx"));
15786            let candidate = evidence.join(format!("{kind}-candidate.pptx"));
15787            fs::copy(&source, &original).expect("copy SHA-bound viewer original");
15788            fs::rename(&unbound, &candidate).expect("bind viewer candidate to SHA");
15789            assert_eq!(sha256(&original), original_sha);
15790            assert_eq!(sha256(&candidate), candidate_sha);
15791
15792            let original_render = render_deck_to_ppm(&original, &evidence, kind, "original");
15793            let candidate_render = render_deck_to_ppm(&candidate, &evidence, kind, "candidate");
15794            let original_bytes = fs::read(&original_render).expect("read original PPM");
15795            let candidate_bytes = fs::read(&candidate_render).expect("read candidate PPM");
15796            let normalized_mae = normalized_ppm_mae(&original_bytes, &candidate_bytes);
15797            assert!(
15798                normalized_mae <= PLOT_RENDER_NORMALIZED_MAE_THRESHOLD,
15799                "{deck}: normalized RGB MAE {normalized_mae:.8} exceeds {:.8}",
15800                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
15801            );
15802            eprintln!(
15803                "F-121 {kind} viewer gate original deck {original_sha}, candidate deck {candidate_sha}, original render {}, candidate render {}, normalized RGB MAE {normalized_mae:.8} <= {:.8}",
15804                sha256(&original_render),
15805                sha256(&candidate_render),
15806                PLOT_RENDER_NORMALIZED_MAE_THRESHOLD
15807            );
15808        }
15809        fs::remove_dir_all(&temp_root).expect("remove F-121 viewer evidence");
15810    }
15811
15812    fn render_deck_to_ppm(deck: &Path, evidence: &Path, kind: &str, side: &str) -> PathBuf {
15813        let profile = std::env::temp_dir().join(format!(
15814            "rpptx-chart-f121-{kind}-{side}-profile-{}",
15815            std::process::id()
15816        ));
15817        if profile.exists() {
15818            fs::remove_dir_all(&profile).expect("remove stale F-121 viewer profile");
15819        }
15820        let profile_argument = format!("-env:UserInstallation=file://{}", profile.display());
15821        let conversion = Command::new("soffice")
15822            .args([
15823                "--headless",
15824                &profile_argument,
15825                "--convert-to",
15826                "pdf:impress_pdf_Export",
15827                "--outdir",
15828            ])
15829            .arg(evidence)
15830            .arg(deck)
15831            .output()
15832            .expect("run pinned LibreOffice F-121 viewer gate");
15833        assert!(
15834            conversion.status.success(),
15835            "{}: LibreOffice conversion failed: {}",
15836            deck.display(),
15837            String::from_utf8_lossy(&conversion.stderr)
15838        );
15839        let pdf = deck.with_extension("pdf");
15840        assert!(
15841            pdf.is_file(),
15842            "LibreOffice did not create {}",
15843            pdf.display()
15844        );
15845        let prefix = evidence.join(format!("{kind}-{side}"));
15846        let raster = Command::new("pdftoppm")
15847            .args(["-f", "1", "-singlefile", "-r", "150"])
15848            .arg(&pdf)
15849            .arg(&prefix)
15850            .output()
15851            .expect("run pinned Poppler F-121 raster gate");
15852        assert!(
15853            raster.status.success(),
15854            "{}: pdftoppm failed: {}",
15855            pdf.display(),
15856            String::from_utf8_lossy(&raster.stderr)
15857        );
15858        if profile.exists() {
15859            fs::remove_dir_all(&profile).expect("remove F-121 viewer profile");
15860        }
15861        prefix.with_extension("ppm")
15862    }
15863
15864    fn normalized_ppm_mae(left: &[u8], right: &[u8]) -> f64 {
15865        let (left_width, left_height, left_pixels) = ppm_pixels(left);
15866        let (right_width, right_height, right_pixels) = ppm_pixels(right);
15867        assert_eq!((left_width, left_height), (right_width, right_height));
15868        assert_eq!(left_pixels.len(), right_pixels.len());
15869        let difference: u64 = left_pixels
15870            .iter()
15871            .zip(right_pixels)
15872            .map(|(left, right)| u64::from(left.abs_diff(*right)))
15873            .sum();
15874        difference as f64 / (left_pixels.len() as f64 * 255.0)
15875    }
15876
15877    fn ppm_pixels(bytes: &[u8]) -> (usize, usize, &[u8]) {
15878        let mut cursor = 0usize;
15879        let mut tokens = Vec::new();
15880        while tokens.len() < 4 {
15881            while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
15882                cursor += 1;
15883            }
15884            if cursor < bytes.len() && bytes[cursor] == b'#' {
15885                while cursor < bytes.len() && bytes[cursor] != b'\n' {
15886                    cursor += 1;
15887                }
15888                continue;
15889            }
15890            let start = cursor;
15891            while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() {
15892                cursor += 1;
15893            }
15894            tokens.push(std::str::from_utf8(&bytes[start..cursor]).expect("PPM header UTF-8"));
15895        }
15896        assert_eq!(tokens[0], "P6");
15897        let width = tokens[1].parse::<usize>().expect("PPM width");
15898        let height = tokens[2].parse::<usize>().expect("PPM height");
15899        assert_eq!(tokens[3], "255");
15900        assert!(
15901            cursor < bytes.len() && bytes[cursor].is_ascii_whitespace(),
15902            "PPM header must end with whitespace"
15903        );
15904        cursor += 1;
15905        let pixels = &bytes[cursor..];
15906        assert_eq!(pixels.len(), width * height * 3, "PPM pixel length");
15907        (width, height, pixels)
15908    }
15909}