use std::fmt::Write;
use crate::error::PptxResult;
use crate::oxml::ns::{NS_A, NS_C, NS_R};
use super::super::data::ComboChartData;
use super::ChartXmlWriter;
const CAT_AX_ID: &str = "-2068027336";
const PRIMARY_VAL_AX_ID: &str = "-2113994440";
const SECONDARY_VAL_AX_ID: &str = "2140495176";
impl ChartXmlWriter {
pub fn write_combo(data: &ComboChartData) -> PptxResult<String> {
let mut xml = String::with_capacity(8192);
write_str!(
xml,
"<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n\
<c:chartSpace xmlns:c=\"{NS_C}\" xmlns:a=\"{NS_A}\" xmlns:r=\"{NS_R}\">\n\
\x20 <c:date1904 val=\"0\"/>\n\
\x20 <c:chart>\n\
\x20 <c:autoTitleDeleted val=\"0\"/>\n\
\x20 <c:plotArea>\n"
);
Self::write_combo_bar_section(&mut xml, data);
Self::write_combo_line_section(&mut xml, data);
Self::cat_ax_to(&mut xml, CAT_AX_ID, PRIMARY_VAL_AX_ID, "b");
write_str!(
xml,
"\x20 <c:valAx>\n\
\x20 <c:axId val=\"{PRIMARY_VAL_AX_ID}\"/>\n\
\x20 <c:scaling/>\n\
\x20 <c:delete val=\"0\"/>\n\
\x20 <c:axPos val=\"l\"/>\n\
\x20 <c:majorGridlines/>\n\
\x20 <c:majorTickMark val=\"out\"/>\n\
\x20 <c:minorTickMark val=\"none\"/>\n\
\x20 <c:tickLblPos val=\"nextTo\"/>\n\
\x20 <c:crossAx val=\"{CAT_AX_ID}\"/>\n\
\x20 <c:crosses val=\"autoZero\"/>\n\
\x20 </c:valAx>\n"
);
write_str!(
xml,
"\x20 <c:valAx>\n\
\x20 <c:axId val=\"{SECONDARY_VAL_AX_ID}\"/>\n\
\x20 <c:scaling/>\n\
\x20 <c:delete val=\"0\"/>\n\
\x20 <c:axPos val=\"r\"/>\n\
\x20 <c:majorTickMark val=\"out\"/>\n\
\x20 <c:minorTickMark val=\"none\"/>\n\
\x20 <c:tickLblPos val=\"nextTo\"/>\n\
\x20 <c:crossAx val=\"{CAT_AX_ID}\"/>\n\
\x20 <c:crosses val=\"max\"/>\n\
\x20 </c:valAx>\n"
);
write_str!(
xml,
"\x20 </c:plotArea>\n\
\x20 <c:legend>\n\
\x20 <c:legendPos val=\"b\"/>\n\
\x20 <c:layout/>\n\
\x20 <c:overlay val=\"0\"/>\n\
\x20 </c:legend>\n\
\x20 <c:plotVisOnly val=\"1\"/>\n\
\x20 <c:dispBlanksAs val=\"gap\"/>\n\
\x20 </c:chart>\n"
);
Self::txpr_to(&mut xml);
xml.push_str("</c:chartSpace>\n");
Ok(xml)
}
fn write_combo_bar_section(xml: &mut String, data: &ComboChartData) {
write_str!(
xml,
"\x20 <c:barChart>\n\
\x20 <c:barDir val=\"col\"/>\n\
\x20 <c:grouping val=\"clustered\"/>\n"
);
let categories = data.categories();
for series in data.bar_series() {
let idx = series.index();
write_str!(
xml,
"\x20 <c:ser>\n\
\x20 <c:idx val=\"{idx}\"/>\n\
\x20 <c:order val=\"{idx}\"/>\n"
);
Self::tx_to(xml, series.name());
Self::combo_cat_to(xml, categories);
Self::val_to(xml, series.as_category_series());
xml.push_str("\x20 </c:ser>\n");
}
write_str!(
xml,
"\x20 <c:axId val=\"{CAT_AX_ID}\"/>\n\
\x20 <c:axId val=\"{PRIMARY_VAL_AX_ID}\"/>\n\
\x20 </c:barChart>\n"
);
}
fn write_combo_line_section(xml: &mut String, data: &ComboChartData) {
write_str!(
xml,
"\x20 <c:lineChart>\n\
\x20 <c:grouping val=\"standard\"/>\n\
\x20 <c:varyColors val=\"0\"/>\n"
);
let categories = data.categories();
for series in data.line_series() {
let idx = series.index();
write_str!(
xml,
"\x20 <c:ser>\n\
\x20 <c:idx val=\"{idx}\"/>\n\
\x20 <c:order val=\"{idx}\"/>\n"
);
Self::tx_to(xml, series.name());
xml.push_str(
" <c:marker>\n\
\x20 <c:symbol val=\"none\"/>\n\
\x20 </c:marker>\n",
);
Self::combo_cat_to(xml, categories);
Self::val_to(xml, series.as_category_series());
xml.push_str(
"\x20 <c:smooth val=\"0\"/>\n\
\x20 </c:ser>\n",
);
}
write_str!(
xml,
"\x20 <c:marker val=\"1\"/>\n\
\x20 <c:smooth val=\"0\"/>\n\
\x20 <c:axId val=\"{CAT_AX_ID}\"/>\n\
\x20 <c:axId val=\"{SECONDARY_VAL_AX_ID}\"/>\n\
\x20 </c:lineChart>\n"
);
}
fn combo_cat_to(w: &mut String, categories: &[String]) {
let count = categories.len();
write_str!(
w,
"\x20 <c:cat>\n\
\x20 <c:strRef>\n\
\x20 <c:f>Sheet1!$A$2:$A${}</c:f>\n\
\x20 <c:strCache>\n\
\x20 <c:ptCount val=\"{count}\"/>\n",
count + 1
);
for (idx, cat) in categories.iter().enumerate() {
write_str!(
w,
"\x20 <c:pt idx=\"{idx}\">\n\
\x20 <c:v>"
);
super::xml_escape_to(w, cat);
w.push_str(
"</c:v>\n\
\x20 </c:pt>\n",
);
}
w.push_str(
"\x20 </c:strCache>\n\
\x20 </c:strRef>\n\
\x20 </c:cat>\n",
);
}
}
#[cfg(test)]
mod tests {
use crate::chart::data::{ComboChartData, ComboSeriesType};
use crate::chart::xmlwriter::ChartXmlWriter;
#[test]
fn test_combo_chart_contains_both_chart_types() {
let mut data = ComboChartData::new();
data.add_category("Q1");
data.add_category("Q2");
data.add_series("Revenue", ComboSeriesType::Bar, &[100.0, 150.0]);
data.add_series("Growth %", ComboSeriesType::Line, &[10.0, 15.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:chartSpace"));
assert!(xml.contains("<c:barChart>"));
assert!(xml.contains("<c:lineChart>"));
assert!(xml.contains("</c:chartSpace>"));
}
#[test]
fn test_combo_chart_bar_properties() {
let mut data = ComboChartData::new();
data.add_category("A");
data.add_series("Bars", ComboSeriesType::Bar, &[42.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:barDir val=\"col\"/>"));
assert!(xml.contains("<c:grouping val=\"clustered\"/>"));
assert!(xml.contains("<c:v>42</c:v>"));
}
#[test]
fn test_combo_chart_line_properties() {
let mut data = ComboChartData::new();
data.add_category("A");
data.add_series("Lines", ComboSeriesType::Line, &[7.5]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:grouping val=\"standard\"/>"));
assert!(xml.contains("<c:symbol val=\"none\"/>"));
assert!(xml.contains("<c:smooth val=\"0\"/>"));
}
#[test]
fn test_combo_chart_dual_axes() {
let mut data = ComboChartData::new();
data.add_category("X");
data.add_series("Bar", ComboSeriesType::Bar, &[1.0]);
data.add_series("Line", ComboSeriesType::Line, &[2.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:axPos val=\"l\"/>"));
assert!(xml.contains("<c:axPos val=\"r\"/>"));
let val_ax_count = xml.matches("<c:valAx>").count();
assert_eq!(val_ax_count, 2, "expected 2 value axes, got {val_ax_count}");
}
#[test]
fn test_combo_chart_series_indices() {
let mut data = ComboChartData::new();
data.add_category("A");
data.add_series("S0", ComboSeriesType::Bar, &[1.0]);
data.add_series("S1", ComboSeriesType::Line, &[2.0]);
data.add_series("S2", ComboSeriesType::Bar, &[3.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:order val=\"0\"/>"));
assert!(xml.contains("<c:order val=\"1\"/>"));
assert!(xml.contains("<c:order val=\"2\"/>"));
}
#[test]
fn test_combo_chart_category_labels() {
let mut data = ComboChartData::new();
data.add_category("Jan");
data.add_category("Feb");
data.add_category("Mar");
data.add_series("S", ComboSeriesType::Bar, &[1.0, 2.0, 3.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:v>Jan</c:v>"));
assert!(xml.contains("<c:v>Feb</c:v>"));
assert!(xml.contains("<c:v>Mar</c:v>"));
assert!(xml.contains("<c:ptCount val=\"3\"/>"));
}
#[test]
fn test_combo_chart_xml_escape() {
let mut data = ComboChartData::new();
data.add_category("A & B");
data.add_series("Sales & Revenue", ComboSeriesType::Bar, &[1.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("Sales & Revenue"));
assert!(xml.contains("A & B"));
}
#[test]
fn test_combo_chart_to_xml_method() {
let mut data = ComboChartData::new();
data.add_category("X");
data.add_series("Bar", ComboSeriesType::Bar, &[10.0]);
data.add_series("Line", ComboSeriesType::Line, &[20.0]);
let xml = data.to_xml().unwrap();
assert!(xml.contains("<c:barChart>"));
assert!(xml.contains("<c:lineChart>"));
}
#[test]
fn test_combo_chart_legend() {
let mut data = ComboChartData::new();
data.add_category("X");
data.add_series("S", ComboSeriesType::Bar, &[1.0]);
let xml = ChartXmlWriter::write_combo(&data).unwrap();
assert!(xml.contains("<c:legend>"));
assert!(xml.contains("<c:legendPos val=\"b\"/>"));
}
}