1use super::xmlchemy::XmlElement;
6
7#[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#[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#[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#[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#[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#[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 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#[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#[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#[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('&', "&")
330 .replace('<', "<")
331 .replace('>', ">")
332 .replace('"', """)
333 .replace('\'', "'")
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}