Skip to main content

ppt_rs/oxml/chart/
mod.rs

1//! Chart XML elements for OOXML
2//!
3//! Provides types for parsing and generating DrawingML chart elements.
4
5use super::xmlchemy::XmlElement;
6
7/// Chart type enumeration
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ChartKind {
10    Bar,
11    Column,
12    Line,
13    Pie,
14    Area,
15    Scatter,
16    Doughnut,
17    Radar,
18}
19
20impl ChartKind {
21    pub fn xml_element(&self) -> &'static str {
22        match self {
23            ChartKind::Bar => "c:barChart",
24            ChartKind::Column => "c:barChart",
25            ChartKind::Line => "c:lineChart",
26            ChartKind::Pie => "c:pieChart",
27            ChartKind::Area => "c:areaChart",
28            ChartKind::Scatter => "c:scatterChart",
29            ChartKind::Doughnut => "c:doughnutChart",
30            ChartKind::Radar => "c:radarChart",
31        }
32    }
33
34    pub fn from_element(name: &str) -> Option<Self> {
35        match name {
36            "barChart" => Some(ChartKind::Bar),
37            "lineChart" => Some(ChartKind::Line),
38            "pieChart" => Some(ChartKind::Pie),
39            "areaChart" => Some(ChartKind::Area),
40            "scatterChart" => Some(ChartKind::Scatter),
41            "doughnutChart" => Some(ChartKind::Doughnut),
42            "radarChart" => Some(ChartKind::Radar),
43            _ => None,
44        }
45    }
46}
47
48/// Data point value
49#[derive(Debug, Clone)]
50pub struct DataPoint {
51    pub index: u32,
52    pub value: f64,
53}
54
55impl DataPoint {
56    pub fn new(index: u32, value: f64) -> Self {
57        DataPoint { index, value }
58    }
59
60    pub fn to_xml(&self) -> String {
61        format!(r#"<c:pt idx="{}"><c:v>{}</c:v></c:pt>"#, self.index, self.value)
62    }
63}
64
65/// Category (string) value
66#[derive(Debug, Clone)]
67pub struct CategoryPoint {
68    pub index: u32,
69    pub value: String,
70}
71
72impl CategoryPoint {
73    pub fn new(index: u32, value: &str) -> Self {
74        CategoryPoint { index, value: value.to_string() }
75    }
76
77    pub fn to_xml(&self) -> String {
78        format!(r#"<c:pt idx="{}"><c:v>{}</c:v></c:pt>"#, self.index, escape_xml(&self.value))
79    }
80}
81
82/// Numeric data reference
83#[derive(Debug, Clone)]
84pub struct NumericData {
85    pub formula: String,
86    pub points: Vec<DataPoint>,
87}
88
89impl NumericData {
90    pub fn new(formula: &str) -> Self {
91        NumericData {
92            formula: formula.to_string(),
93            points: Vec::new(),
94        }
95    }
96
97    pub fn add_point(mut self, index: u32, value: f64) -> Self {
98        self.points.push(DataPoint::new(index, value));
99        self
100    }
101
102    pub fn from_values(values: &[f64]) -> Self {
103        let mut data = NumericData::new("Sheet1!$B$2");
104        for (i, &v) in values.iter().enumerate() {
105            data.points.push(DataPoint::new(i as u32, v));
106        }
107        data
108    }
109
110    pub fn to_xml(&self) -> String {
111        let mut xml = format!(
112            r#"<c:numRef><c:f>{}</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="{}"/>"#,
113            self.formula,
114            self.points.len()
115        );
116        for pt in &self.points {
117            xml.push_str(&pt.to_xml());
118        }
119        xml.push_str("</c:numCache></c:numRef>");
120        xml
121    }
122}
123
124/// String data reference
125#[derive(Debug, Clone)]
126pub struct StringData {
127    pub formula: String,
128    pub points: Vec<CategoryPoint>,
129}
130
131impl StringData {
132    pub fn new(formula: &str) -> Self {
133        StringData {
134            formula: formula.to_string(),
135            points: Vec::new(),
136        }
137    }
138
139    pub fn from_categories(categories: &[&str]) -> Self {
140        let mut data = StringData::new("Sheet1!$A$2");
141        for (i, &cat) in categories.iter().enumerate() {
142            data.points.push(CategoryPoint::new(i as u32, cat));
143        }
144        data
145    }
146
147    pub fn to_xml(&self) -> String {
148        let mut xml = format!(
149            r#"<c:strRef><c:f>{}</c:f><c:strCache><c:ptCount val="{}"/>"#,
150            self.formula,
151            self.points.len()
152        );
153        for pt in &self.points {
154            xml.push_str(&pt.to_xml());
155        }
156        xml.push_str("</c:strCache></c:strRef>");
157        xml
158    }
159}
160
161/// Chart series
162#[derive(Debug, Clone)]
163pub struct ChartSeries {
164    pub index: u32,
165    pub name: String,
166    pub values: NumericData,
167    pub categories: Option<StringData>,
168}
169
170impl ChartSeries {
171    pub fn new(index: u32, name: &str, values: NumericData) -> Self {
172        ChartSeries {
173            index,
174            name: name.to_string(),
175            values,
176            categories: None,
177        }
178    }
179
180    pub fn with_categories(mut self, categories: StringData) -> Self {
181        self.categories = Some(categories);
182        self
183    }
184
185    pub fn parse(elem: &XmlElement) -> Option<Self> {
186        let index = elem.find("idx")
187            .and_then(|e| e.attr("val"))
188            .and_then(|v| v.parse().ok())
189            .unwrap_or(0);
190
191        let name = elem.find_descendant("t")
192            .map(|t| t.text_content())
193            .unwrap_or_default();
194
195        // Parse values
196        let values = NumericData::new("Sheet1!$B$2");
197
198        Some(ChartSeries {
199            index,
200            name,
201            values,
202            categories: None,
203        })
204    }
205
206    pub fn to_xml(&self) -> String {
207        let mut xml = format!(
208            r#"<c:ser><c:idx val="{}"/><c:order val="{}"/><c:tx><c:strRef><c:f>Sheet1!$B$1</c:f><c:strCache><c:ptCount val="1"/><c:pt idx="0"><c:v>{}</c:v></c:pt></c:strCache></c:strRef></c:tx>"#,
209            self.index,
210            self.index,
211            escape_xml(&self.name)
212        );
213
214        if let Some(ref cats) = self.categories {
215            xml.push_str("<c:cat>");
216            xml.push_str(&cats.to_xml());
217            xml.push_str("</c:cat>");
218        }
219
220        xml.push_str("<c:val>");
221        xml.push_str(&self.values.to_xml());
222        xml.push_str("</c:val>");
223        xml.push_str("</c:ser>");
224        xml
225    }
226}
227
228/// Chart axis
229#[derive(Debug, Clone)]
230pub struct ChartAxis {
231    pub id: u32,
232    pub position: String,
233    pub cross_axis_id: u32,
234    pub delete: bool,
235}
236
237impl ChartAxis {
238    pub fn category(id: u32, cross_id: u32) -> Self {
239        ChartAxis {
240            id,
241            position: "b".to_string(),
242            cross_axis_id: cross_id,
243            delete: false,
244        }
245    }
246
247    pub fn value(id: u32, cross_id: u32) -> Self {
248        ChartAxis {
249            id,
250            position: "l".to_string(),
251            cross_axis_id: cross_id,
252            delete: false,
253        }
254    }
255
256    pub fn to_category_xml(&self) -> String {
257        format!(
258            r#"<c:catAx><c:axId val="{}"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="{}"/><c:axPos val="{}"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:crossAx val="{}"/><c:crosses val="autoZero"/></c:catAx>"#,
259            self.id,
260            if self.delete { "1" } else { "0" },
261            self.position,
262            self.cross_axis_id
263        )
264    }
265
266    pub fn to_value_xml(&self) -> String {
267        format!(
268            r#"<c:valAx><c:axId val="{}"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="{}"/><c:axPos val="{}"/><c:majorGridlines/><c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:crossAx val="{}"/><c:crosses val="autoZero"/></c:valAx>"#,
269            self.id,
270            if self.delete { "1" } else { "0" },
271            self.position,
272            self.cross_axis_id
273        )
274    }
275}
276
277/// Chart legend
278#[derive(Debug, Clone)]
279pub struct ChartLegend {
280    pub position: String,
281    pub overlay: bool,
282}
283
284impl ChartLegend {
285    pub fn new(position: &str) -> Self {
286        ChartLegend {
287            position: position.to_string(),
288            overlay: false,
289        }
290    }
291
292    pub fn right() -> Self {
293        Self::new("r")
294    }
295
296    pub fn bottom() -> Self {
297        Self::new("b")
298    }
299
300    pub fn to_xml(&self) -> String {
301        format!(
302            r#"<c:legend><c:legendPos val="{}"/><c:overlay val="{}"/></c:legend>"#,
303            self.position,
304            if self.overlay { "1" } else { "0" }
305        )
306    }
307}
308
309/// Chart title
310#[derive(Debug, Clone)]
311pub struct ChartTitle {
312    pub text: String,
313}
314
315impl ChartTitle {
316    pub fn new(text: &str) -> Self {
317        ChartTitle { text: text.to_string() }
318    }
319
320    pub fn to_xml(&self) -> String {
321        format!(
322            r#"<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="en-US"/><a:t>{}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val="0"/></c:title>"#,
323            escape_xml(&self.text)
324        )
325    }
326}
327
328fn escape_xml(s: &str) -> String {
329    s.replace('&', "&amp;")
330        .replace('<', "&lt;")
331        .replace('>', "&gt;")
332        .replace('"', "&quot;")
333        .replace('\'', "&apos;")
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_chart_kind() {
342        assert_eq!(ChartKind::Bar.xml_element(), "c:barChart");
343        assert_eq!(ChartKind::Pie.xml_element(), "c:pieChart");
344    }
345
346    #[test]
347    fn test_data_point() {
348        let pt = DataPoint::new(0, 42.5);
349        let xml = pt.to_xml();
350        assert!(xml.contains("idx=\"0\""));
351        assert!(xml.contains("42.5"));
352    }
353
354    #[test]
355    fn test_numeric_data() {
356        let data = NumericData::from_values(&[10.0, 20.0, 30.0]);
357        let xml = data.to_xml();
358        assert!(xml.contains("numRef"));
359        assert!(xml.contains("ptCount val=\"3\""));
360    }
361
362    #[test]
363    fn test_chart_series() {
364        let values = NumericData::from_values(&[100.0, 200.0]);
365        let series = ChartSeries::new(0, "Sales", values);
366        let xml = series.to_xml();
367        
368        assert!(xml.contains("<c:ser>"));
369        assert!(xml.contains("Sales"));
370    }
371
372    #[test]
373    fn test_chart_legend() {
374        let legend = ChartLegend::right();
375        let xml = legend.to_xml();
376        assert!(xml.contains("legendPos val=\"r\""));
377    }
378
379    #[test]
380    fn test_chart_title() {
381        let title = ChartTitle::new("Revenue Report");
382        let xml = title.to_xml();
383        assert!(xml.contains("Revenue Report"));
384    }
385}