1use crate::hash::FastMap;
7use crate::mce::children;
8use crate::slide::parse_text_body;
9use crate::xml::{unescape_attr, Event, Ns, Reader, Start, XmlError};
10
11#[derive(Clone, Debug, Default, PartialEq, Eq)]
13pub struct Series {
14 pub name: Option<String>,
15 pub categories: Vec<String>,
17 pub values: Vec<String>,
19}
20
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct ChartData {
24 pub title: Option<String>,
25 pub kinds: Vec<String>,
27 pub category_axis_title: Option<String>,
28 pub value_axis_title: Option<String>,
29 pub series: Vec<Series>,
30}
31
32impl ChartData {
33 pub fn is_empty(&self) -> bool {
35 self.title.is_none()
36 && self.series.iter().all(|series| {
37 series.name.is_none() && series.categories.is_empty() && series.values.is_empty()
38 })
39 }
40
41 pub fn shares_categories(&self) -> bool {
43 let Some(first) = self.series.first() else {
44 return false;
45 };
46 !first.categories.is_empty()
47 && self
48 .series
49 .iter()
50 .all(|series| series.categories == first.categories)
51 }
52
53 pub fn rows(&self) -> Vec<Vec<String>> {
58 let mut rows = Vec::new();
59 if self.shares_categories() {
60 if self.series.iter().any(|series| series.name.is_some()) {
61 let mut header = vec![String::new()];
62 header.extend(
63 self.series
64 .iter()
65 .map(|series| series.name.clone().unwrap_or_default()),
66 );
67 rows.push(header);
68 }
69 for (index, category) in self.series[0].categories.iter().enumerate() {
70 let mut row = vec![category.clone()];
71 row.extend(
72 self.series
73 .iter()
74 .map(|series| series.values.get(index).cloned().unwrap_or_default()),
75 );
76 rows.push(row);
77 }
78 return rows;
79 }
80 for series in &self.series {
81 let mut row = vec![series.name.clone().unwrap_or_default()];
82 match series.categories.is_empty() {
83 true => row.extend(series.values.iter().cloned()),
84 false => row.extend(
85 series
86 .categories
87 .iter()
88 .zip(
89 series
90 .values
91 .iter()
92 .chain(std::iter::repeat(&String::new())),
93 )
94 .map(|(category, value)| format!("{category}: {value}")),
95 ),
96 }
97 rows.push(row);
98 }
99 rows
100 }
101
102 pub fn write_text(&self, separator: &str, out: &mut String) {
104 let start = out.len();
105 if let Some(title) = &self.title {
106 out.push_str(title.trim());
107 }
108 for row in self.rows() {
109 if out.len() > start {
110 out.push('\n');
111 }
112 out.push_str(&row.join(separator));
113 }
114 }
115}
116
117pub fn parse_chart(xml: &[u8]) -> Result<ChartData, XmlError> {
119 let mut reader = Reader::new(xml);
120 let root = loop {
121 match reader.next()? {
122 Event::Start(start) => break start,
123 Event::Eof => return Ok(ChartData::default()),
124 _ => {}
125 }
126 };
127 let mut chart = ChartData::default();
128 match root.name.ns {
129 Ns::ChartEx => parse_extended(&mut reader, &mut chart)?,
130 _ => parse_chart_space(&mut reader, &root, &mut chart)?,
131 }
132 Ok(chart)
133}
134
135fn parse_chart_space<'a>(
137 reader: &mut Reader<'a>,
138 root: &Start<'a>,
139 chart: &mut ChartData,
140) -> Result<(), XmlError> {
141 if root.name.is(Ns::Chart, b"chart") {
142 return parse_c_chart(reader, chart);
143 }
144 children(
145 reader,
146 &mut |reader, child| match child.name.is(Ns::Chart, b"chart") {
147 true => parse_c_chart(reader, chart),
148 false => reader.skip_element(),
149 },
150 )
151}
152
153fn parse_c_chart<'a>(reader: &mut Reader<'a>, chart: &mut ChartData) -> Result<(), XmlError> {
154 children(reader, &mut |reader, child| {
155 if child.name.ns != Ns::Chart {
156 return reader.skip_element();
157 }
158 match child.name.local {
159 b"title" => {
160 chart.title = title_text(reader)?;
161 Ok(())
162 }
163 b"plotArea" => parse_plot_area(reader, chart),
164 _ => reader.skip_element(),
165 }
166 })
167}
168
169fn parse_plot_area<'a>(reader: &mut Reader<'a>, chart: &mut ChartData) -> Result<(), XmlError> {
170 children(reader, &mut |reader, child| {
171 if child.name.ns != Ns::Chart {
172 return reader.skip_element();
173 }
174 let local = String::from_utf8_lossy(child.name.local).into_owned();
175 if local.ends_with("Chart") {
176 chart.kinds.push(local);
177 return children(
178 reader,
179 &mut |reader, item| match item.name.is(Ns::Chart, b"ser") {
180 true => {
181 let series = parse_series(reader)?;
182 chart.series.push(series);
183 Ok(())
184 }
185 false => reader.skip_element(),
186 },
187 );
188 }
189 match child.name.local {
190 b"catAx" | b"dateAx" | b"serAx" => {
191 let title = axis_title(reader)?;
192 if chart.category_axis_title.is_none() {
193 chart.category_axis_title = title;
194 }
195 Ok(())
196 }
197 b"valAx" => {
198 let title = axis_title(reader)?;
199 if chart.value_axis_title.is_none() {
200 chart.value_axis_title = title;
201 }
202 Ok(())
203 }
204 _ => reader.skip_element(),
205 }
206 })
207}
208
209fn axis_title<'a>(reader: &mut Reader<'a>) -> Result<Option<String>, XmlError> {
210 let mut title = None;
211 children(
212 reader,
213 &mut |reader, child| match child.name.is(Ns::Chart, b"title") {
214 true => {
215 title = title_text(reader)?;
216 Ok(())
217 }
218 false => reader.skip_element(),
219 },
220 )?;
221 Ok(title)
222}
223
224fn title_text<'a>(reader: &mut Reader<'a>) -> Result<Option<String>, XmlError> {
226 let mut text = None;
227 children(reader, &mut |reader, child| {
228 if !child.name.is(Ns::Chart, b"tx") {
229 return reader.skip_element();
230 }
231 children(reader, &mut |reader, inner| {
232 if inner.name.is(Ns::Chart, b"rich") {
233 let body = parse_text_body(reader)?.text();
234 if !body.trim().is_empty() {
235 text = Some(body);
236 }
237 return Ok(());
238 }
239 if inner.name.is(Ns::Chart, b"strRef") {
240 let points = string_points(reader)?;
241 let joined = points.join(" ");
242 if !joined.trim().is_empty() {
243 text = Some(joined);
244 }
245 return Ok(());
246 }
247 reader.skip_element()
248 })
249 })?;
250 Ok(text)
251}
252
253fn parse_series<'a>(reader: &mut Reader<'a>) -> Result<Series, XmlError> {
254 let mut series = Series::default();
255 children(reader, &mut |reader, child| {
256 if child.name.ns != Ns::Chart {
257 return reader.skip_element();
258 }
259 match child.name.local {
260 b"tx" => {
261 let points = data_points(reader)?;
262 series.name = Some(points.join(" ")).filter(|name| !name.trim().is_empty());
263 Ok(())
264 }
265 b"cat" | b"xVal" => {
266 series.categories = data_points(reader)?;
267 Ok(())
268 }
269 b"val" | b"yVal" => {
270 series.values = data_points(reader)?;
271 Ok(())
272 }
273 _ => reader.skip_element(),
274 }
275 })?;
276 Ok(series)
277}
278
279fn data_points<'a>(reader: &mut Reader<'a>) -> Result<Vec<String>, XmlError> {
283 let mut points = Vec::new();
284 children(reader, &mut |reader, child| {
285 if child.name.ns != Ns::Chart {
286 return reader.skip_element();
287 }
288 match child.name.local {
289 b"v" => {
290 let mut text = String::new();
291 reader.text_content(&mut text)?;
292 points.push(text);
293 Ok(())
294 }
295 b"strRef" | b"numRef" | b"strLit" | b"numLit" | b"multiLvlStrRef" => {
296 points.extend(string_points(reader)?);
297 Ok(())
298 }
299 _ => reader.skip_element(),
300 }
301 })?;
302 Ok(points)
303}
304
305fn string_points<'a>(reader: &mut Reader<'a>) -> Result<Vec<String>, XmlError> {
308 let mut indexed: Vec<(u32, String)> = Vec::new();
309 let mut level = 0usize;
310 let mut kept_level = 0usize;
311 collect_points(reader, &mut indexed, &mut level, &mut kept_level)?;
312 indexed.sort_by_key(|(index, _)| *index);
313 Ok(indexed.into_iter().map(|(_, value)| value).collect())
314}
315
316fn collect_points<'a>(
317 reader: &mut Reader<'a>,
318 indexed: &mut Vec<(u32, String)>,
319 level: &mut usize,
320 kept_level: &mut usize,
321) -> Result<(), XmlError> {
322 children(reader, &mut |reader, child| {
323 if child.name.ns != Ns::Chart {
324 return reader.skip_element();
325 }
326 match child.name.local {
327 b"pt" => {
328 let index = reader
329 .attr(&child, Ns::None, b"idx")
330 .and_then(|raw| std::str::from_utf8(raw).ok()?.trim().parse().ok())
331 .unwrap_or(indexed.len() as u32);
332 let mut value = String::new();
333 children(
334 reader,
335 &mut |reader, inner| match inner.name.is(Ns::Chart, b"v") {
336 true => reader.text_content(&mut value),
337 false => reader.skip_element(),
338 },
339 )?;
340 if *level == *kept_level {
341 indexed.push((index, value));
342 }
343 Ok(())
344 }
345 b"lvl" => {
346 *level += 1;
347 if *level > *kept_level {
348 *kept_level = *level;
349 indexed.clear();
350 }
351 collect_points(reader, indexed, level, kept_level)?;
352 *level -= 1;
353 Ok(())
354 }
355 b"f" | b"ptCount" | b"formatCode" | b"extLst" => reader.skip_element(),
356 _ => collect_points(reader, indexed, level, kept_level),
357 }
358 })
359}
360
361fn parse_extended<'a>(reader: &mut Reader<'a>, chart: &mut ChartData) -> Result<(), XmlError> {
364 let mut data: FastMap<String, (Vec<String>, Vec<String>)> = FastMap::default();
365 let mut order: Vec<String> = Vec::new();
366 children(reader, &mut |reader, child| {
367 if child.name.ns != Ns::ChartEx {
368 return reader.skip_element();
369 }
370 match child.name.local {
371 b"chartData" => children(reader, &mut |reader, item| {
372 if !item.name.is(Ns::ChartEx, b"data") {
373 return reader.skip_element();
374 }
375 let id = reader
376 .attr(&item, Ns::None, b"id")
377 .map(unescape_attr)
378 .unwrap_or_default();
379 let mut categories = Vec::new();
380 let mut values = Vec::new();
381 children(reader, &mut |reader, dim| {
382 if dim.name.is(Ns::ChartEx, b"strDim") {
383 categories = extended_points(reader)?;
384 return Ok(());
385 }
386 if dim.name.is(Ns::ChartEx, b"numDim") && values.is_empty() {
387 values = extended_points(reader)?;
388 return Ok(());
389 }
390 reader.skip_element()
391 })?;
392 order.push(id.clone());
393 data.insert(id, (categories, values));
394 Ok(())
395 }),
396 b"chart" => children(reader, &mut |reader, item| {
397 if item.name.is(Ns::ChartEx, b"title") {
398 chart.title = extended_text(reader)?;
399 return Ok(());
400 }
401 if !item.name.is(Ns::ChartEx, b"plotArea") {
402 return reader.skip_element();
403 }
404 children(reader, &mut |reader, region| {
405 if !region.name.is(Ns::ChartEx, b"plotAreaRegion") {
406 return reader.skip_element();
407 }
408 children(reader, &mut |reader, series| {
409 if !series.name.is(Ns::ChartEx, b"series") {
410 return reader.skip_element();
411 }
412 if let Some(layout) = reader.attr(&series, Ns::None, b"layoutId") {
413 chart.kinds.push(unescape_attr(layout));
414 }
415 let mut item = Series::default();
416 let mut data_id = None;
417 children(reader, &mut |reader, part| {
418 if part.name.is(Ns::ChartEx, b"tx") {
419 item.name = extended_text(reader)?;
420 return Ok(());
421 }
422 if part.name.is(Ns::ChartEx, b"dataId") {
423 data_id = reader.attr(&part, Ns::None, b"val").map(unescape_attr);
424 }
425 reader.skip_element()
426 })?;
427 if let Some((categories, values)) =
428 data_id.as_deref().and_then(|id| data.get(id))
429 {
430 item.categories = categories.clone();
431 item.values = values.clone();
432 }
433 chart.series.push(item);
434 Ok(())
435 })
436 })
437 }),
438 _ => reader.skip_element(),
439 }
440 })?;
441 if chart.series.is_empty() {
442 for id in order {
443 if let Some((categories, values)) = data.remove(&id) {
444 chart.series.push(Series {
445 name: None,
446 categories,
447 values,
448 });
449 }
450 }
451 }
452 Ok(())
453}
454
455fn extended_text<'a>(reader: &mut Reader<'a>) -> Result<Option<String>, XmlError> {
457 let mut text = None;
458 children(reader, &mut |reader, child| {
459 if child.name.is(Ns::ChartEx, b"tx") {
460 return children(reader, &mut |reader, inner| {
461 if inner.name.is(Ns::ChartEx, b"txData") {
462 return children(reader, &mut |reader, leaf| match leaf
463 .name
464 .is(Ns::ChartEx, b"v")
465 {
466 true => {
467 let mut value = String::new();
468 reader.text_content(&mut value)?;
469 text = Some(value).filter(|value| !value.trim().is_empty());
470 Ok(())
471 }
472 false => reader.skip_element(),
473 });
474 }
475 if inner.name.is(Ns::ChartEx, b"rich") {
476 let body = parse_text_body(reader)?.text();
477 text = Some(body).filter(|body| !body.trim().is_empty());
478 return Ok(());
479 }
480 reader.skip_element()
481 });
482 }
483 if child.name.is(Ns::ChartEx, b"txData") {
484 return children(
485 reader,
486 &mut |reader, leaf| match leaf.name.is(Ns::ChartEx, b"v") {
487 true => {
488 let mut value = String::new();
489 reader.text_content(&mut value)?;
490 text = Some(value).filter(|value| !value.trim().is_empty());
491 Ok(())
492 }
493 false => reader.skip_element(),
494 },
495 );
496 }
497 if child.name.is(Ns::ChartEx, b"rich") {
498 let body = parse_text_body(reader)?.text();
499 text = Some(body).filter(|body| !body.trim().is_empty());
500 return Ok(());
501 }
502 reader.skip_element()
503 })?;
504 Ok(text)
505}
506
507fn extended_points<'a>(reader: &mut Reader<'a>) -> Result<Vec<String>, XmlError> {
509 let mut levels: Vec<Vec<(u32, String)>> = Vec::new();
510 children(reader, &mut |reader, child| {
511 if !child.name.is(Ns::ChartEx, b"lvl") {
512 return reader.skip_element();
513 }
514 let mut points = Vec::new();
515 children(reader, &mut |reader, pt| {
516 if !pt.name.is(Ns::ChartEx, b"pt") {
517 return reader.skip_element();
518 }
519 let index = reader
520 .attr(&pt, Ns::None, b"idx")
521 .and_then(|raw| std::str::from_utf8(raw).ok()?.trim().parse().ok())
522 .unwrap_or(points.len() as u32);
523 let mut value = String::new();
524 reader.text_content(&mut value)?;
525 points.push((index, value));
526 Ok(())
527 })?;
528 points.sort_by_key(|(index, _)| *index);
529 levels.push(points);
530 Ok(())
531 })?;
532 Ok(levels
533 .into_iter()
534 .last()
535 .unwrap_or_default()
536 .into_iter()
537 .map(|(_, value)| value)
538 .collect())
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 const C: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
546 const A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
547
548 #[test]
549 fn bar_chart_title_series_categories_and_values() {
550 let xml = format!(
551 r#"<c:chartSpace xmlns:c="{C}" xmlns:a="{A}"><c:chart><c:title><c:tx><c:rich><a:bodyPr/><a:p><a:r><a:t>Sales by </a:t></a:r><a:r><a:t>region</a:t></a:r></a:p></c:rich></c:tx></c:title><c:plotArea><c:barChart><c:ser><c:idx val="0"/><c:tx><c:strRef><c:f>Sheet1!$B$1</c:f><c:strCache><c:ptCount val="1"/><c:pt idx="0"><c:v>2023</c:v></c:pt></c:strCache></c:strRef></c:tx><c:cat><c:strRef><c:strCache><c:pt idx="1"><c:v>South</c:v></c:pt><c:pt idx="0"><c:v>North</c:v></c:pt></c:strCache></c:strRef></c:cat><c:val><c:numRef><c:numCache><c:formatCode>General</c:formatCode><c:pt idx="0"><c:v>4.3</c:v></c:pt><c:pt idx="1"><c:v>2.5</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser><c:ser><c:idx val="1"/><c:tx><c:v>2024</c:v></c:tx><c:cat><c:strLit><c:pt idx="0"><c:v>North</c:v></c:pt><c:pt idx="1"><c:v>South</c:v></c:pt></c:strLit></c:cat><c:val><c:numLit><c:pt idx="0"><c:v>5</c:v></c:pt><c:pt idx="1"><c:v>3</c:v></c:pt></c:numLit></c:val></c:ser></c:barChart><c:catAx><c:title><c:tx><c:rich><a:bodyPr/><a:p><a:r><a:t>Region</a:t></a:r></a:p></c:rich></c:tx></c:title></c:catAx><c:valAx><c:title><c:tx><c:rich><a:bodyPr/><a:p><a:r><a:t>Millions</a:t></a:r></a:p></c:rich></c:tx></c:title></c:valAx></c:plotArea></c:chart></c:chartSpace>"#
552 );
553 let chart = parse_chart(xml.as_bytes()).unwrap();
554 assert_eq!(chart.title.as_deref(), Some("Sales by region"));
555 assert_eq!(chart.kinds, ["barChart"]);
556 assert_eq!(chart.category_axis_title.as_deref(), Some("Region"));
557 assert_eq!(chart.value_axis_title.as_deref(), Some("Millions"));
558 assert_eq!(chart.series.len(), 2);
559 assert_eq!(chart.series[0].name.as_deref(), Some("2023"));
560 assert_eq!(chart.series[0].categories, ["North", "South"]);
561 assert_eq!(chart.series[0].values, ["4.3", "2.5"]);
562 assert_eq!(chart.series[1].name.as_deref(), Some("2024"));
563 assert!(chart.shares_categories());
564 let mut text = String::new();
565 chart.write_text("\t", &mut text);
566 assert_eq!(
567 text,
568 "Sales by region\n\t2023\t2024\nNorth\t4.3\t5\nSouth\t2.5\t3"
569 );
570 }
571
572 #[test]
573 fn scatter_series_with_different_points_list_per_series() {
574 let xml = format!(
575 r#"<c:chartSpace xmlns:c="{C}"><c:chart><c:plotArea><c:scatterChart><c:ser><c:tx><c:v>Run A</c:v></c:tx><c:xVal><c:numLit><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="1"><c:v>2</c:v></c:pt></c:numLit></c:xVal><c:yVal><c:numLit><c:pt idx="0"><c:v>10</c:v></c:pt><c:pt idx="1"><c:v>20</c:v></c:pt></c:numLit></c:yVal></c:ser><c:ser><c:tx><c:v>Run B</c:v></c:tx><c:xVal><c:numLit><c:pt idx="0"><c:v>3</c:v></c:pt></c:numLit></c:xVal><c:yVal><c:numLit><c:pt idx="0"><c:v>30</c:v></c:pt></c:numLit></c:yVal></c:ser></c:scatterChart></c:plotArea></c:chart></c:chartSpace>"#
576 );
577 let chart = parse_chart(xml.as_bytes()).unwrap();
578 assert!(chart.title.is_none());
579 assert!(!chart.shares_categories());
580 let mut text = String::new();
581 chart.write_text("\t", &mut text);
582 assert_eq!(text, "Run A\t1: 10\t2: 20\nRun B\t3: 30");
583 }
584
585 #[test]
586 fn extended_charts_read_titles_series_and_dimensions() {
587 let xml = r#"<cx:chartSpace xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><cx:chartData><cx:data id="0"><cx:strDim type="cat"><cx:f>Sheet1!$A$2:$A$4</cx:f><cx:lvl ptCount="3"><cx:pt idx="0">Leaf</cx:pt><cx:pt idx="1">Stem</cx:pt><cx:pt idx="2">Root</cx:pt></cx:lvl></cx:strDim><cx:numDim type="size"><cx:f>Sheet1!$B$2:$B$4</cx:f><cx:lvl ptCount="3" formatCode="General"><cx:pt idx="0">5</cx:pt><cx:pt idx="1">3</cx:pt><cx:pt idx="2">2</cx:pt></cx:lvl></cx:numDim></cx:data></cx:chartData><cx:chart><cx:title pos="t" align="ctr" overlay="0"><cx:tx><cx:txData><cx:v>Plant parts</cx:v></cx:txData></cx:tx></cx:title><cx:plotArea><cx:plotAreaRegion><cx:series layoutId="treemap" uniqueId="{1}"><cx:tx><cx:txData><cx:v>Mass</cx:v></cx:txData></cx:tx><cx:dataId val="0"/></cx:series></cx:plotAreaRegion></cx:plotArea></cx:chart></cx:chartSpace>"#;
588 let chart = parse_chart(xml.as_bytes()).unwrap();
589 assert_eq!(chart.title.as_deref(), Some("Plant parts"));
590 assert_eq!(chart.kinds, ["treemap"]);
591 assert_eq!(chart.series.len(), 1);
592 assert_eq!(chart.series[0].name.as_deref(), Some("Mass"));
593 assert_eq!(chart.series[0].categories, ["Leaf", "Stem", "Root"]);
594 assert_eq!(chart.series[0].values, ["5", "3", "2"]);
595 }
596
597 #[test]
598 fn an_empty_chart_part_is_empty_not_an_error() {
599 let chart = parse_chart(format!(r#"<c:chartSpace xmlns:c="{C}"/>"#).as_bytes()).unwrap();
600 assert!(chart.is_empty());
601 assert!(chart.rows().is_empty());
602 }
603}