diff --git a/Cargo.lock b/Cargo.lock index 8a54a89..f0f7b56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,15 @@ dependencies = [ "toml", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -516,6 +525,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -656,6 +676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1240,6 +1261,15 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libz-rs-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1652,6 +1682,7 @@ dependencies = [ "pulldown-cmark", "regex", "reqwest", + "rust_xlsxwriter", "scraper", "serde", "serde_json", @@ -1659,8 +1690,9 @@ dependencies = [ "thiserror 1.0.69", "url", "uuid", + "windows-sys 0.52.0", "xml-rs", - "zip", + "zip 0.6.6", ] [[package]] @@ -1930,6 +1962,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "rust_xlsxwriter" +version = "0.92.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8b9faf2c68874f865272c92493e9bb811e5fdff197a56ecc4748885ec5a874" +dependencies = [ + "zip 6.0.0", +] + [[package]] name = "rustix" version = "1.1.3" @@ -3292,12 +3333,44 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + [[package]] name = "zmij" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index b2d3259..7b9071d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,9 @@ anyrepair = "0.1" clap = { version = "4.5", features = ["derive"] } pulldown-cmark = "0.10" syntect = "5.2" +rust_xlsxwriter = "0.92.2" +# Windows API for language detection (Windows only) +windows-sys = { version = "0.52", features = ["Win32_System_SystemInformation"], optional = true } # Web2PPT dependencies reqwest = { version = "0.11", features = ["blocking"], optional = true } scraper = { version = "0.18", optional = true } @@ -33,6 +36,7 @@ url = { version = "2.5", optional = true } [features] default = ["web2ppt"] web2ppt = ["reqwest", "scraper", "url"] +windows-lang = ["windows-sys"] [dev-dependencies] insta = "1.34" diff --git a/examples/actual_charts_demo.rs b/examples/actual_charts_demo.rs new file mode 100644 index 0000000..55806ea --- /dev/null +++ b/examples/actual_charts_demo.rs @@ -0,0 +1,92 @@ +//! Actual chart demonstration with real charts in slides + +use ppt_rs::generator::{ + create_pptx_with_content, SlideContent, SlideLayout, + ChartType, ChartSeries, ChartBuilder, +}; + +fn main() -> Result<(), Box> { + println!("=== Creating Presentation with Real Charts ===\n"); + + // Slide 1: Title + let title_slide = SlideContent::new("Real Charts Demo") + .add_bullet("This presentation contains actual charts") + .add_bullet("Not just text descriptions") + .layout(SlideLayout::TitleAndContent); + + // Slide 2: Bar Chart - Actual data + let bar_chart = ChartBuilder::new("Quarterly Sales", ChartType::Bar) + .categories(vec!["Q1", "Q2", "Q3", "Q4"]) + .add_series(ChartSeries::new("2023", vec![100.0, 120.0, 140.0, 160.0])) + .add_series(ChartSeries::new("2024", vec![110.0, 135.0, 155.0, 180.0])) + .position(2675890, 1725930) // Position on slide (WPS reference) + .size(6839585, 3959860) // Size (WPS reference) + .build(); + + let bar_slide = SlideContent::new("Bar Chart: Sales Comparison") + .add_chart(bar_chart) + .add_bullet("2024 shows consistent growth over 2023") + .add_bullet("Q4 is the strongest quarter"); + + // Slide 3: Line Chart - Trend analysis + let line_chart = ChartBuilder::new("Monthly Revenue Trend", ChartType::Line) + .categories(vec!["Jan", "Feb", "Mar", "Apr", "May", "Jun"]) + .add_series(ChartSeries::new("Revenue (K$)", vec![50.0, 55.0, 62.0, 58.0, 68.0, 75.0])) + .position(2675890, 1725930) + .size(6839585, 3959860) + .build(); + + let line_slide = SlideContent::new("Line Chart: Revenue Trend") + .add_chart(line_chart) + .add_bullet("Steady upward trend from January to June") + .add_bullet("Peak in June: $75K"); + + // Slide 4: Pie Chart - Market share + let pie_chart = ChartBuilder::new("Market Share Distribution", ChartType::Pie) + .categories(vec!["Product A", "Product B", "Product C", "Others"]) + .add_series(ChartSeries::new("Share %", vec![35.0, 28.0, 22.0, 15.0])) + .position(2675890, 1725930) + .size(6839585, 3959860) // Use standard WPS reference coordinates + .build(); + + let pie_slide = SlideContent::new("Pie Chart: Market Share") + .add_chart(pie_chart) + .add_bullet("Product A leads with 35% market share") + .add_bullet("Top 3 products account for 85% of market"); + + // Slide 5: Scatter Chart - Correlation analysis + let scatter_chart = ChartBuilder::new("Price vs Sales Correlation", ChartType::Scatter) + .categories(vec!["10", "20", "30", "40", "50", "60"]) // Price points + .add_series(ChartSeries::new("Product Sales", vec![150.0, 120.0, 90.0, 75.0, 60.0, 45.0])) // Sales volume + .position(2675890, 1725930) + .size(6839585, 3959860) + .build(); + + let scatter_slide = SlideContent::new("Scatter Chart: Price vs Sales") + .add_chart(scatter_chart) + .add_bullet("Higher price correlates with lower sales volume") + .add_bullet("Optimal price point appears to be around $10-20"); + + let slides = vec![ + title_slide, + bar_slide, + line_slide, + pie_slide, + scatter_slide, + ]; + + // Generate the PPTX file + let pptx_data = create_pptx_with_content("Real Charts Demo", slides.clone())?; + std::fs::write("real_charts_demo.pptx", pptx_data)?; + + println!("✓ Created real_charts_demo.pptx with:"); + println!(" - Title slide"); + println!(" - Bar chart: Quarterly sales comparison"); + println!(" - Line chart: Monthly revenue trend"); + println!(" - Pie chart: Market share distribution"); + println!(" - Scatter chart: Price vs Sales correlation"); + println!("\nTotal slides: {}", slides.len()); + println!("\nNote: Check if charts are visible in the generated PPTX file"); + + Ok(()) +} \ No newline at end of file diff --git a/src/generator/builder.rs b/src/generator/builder.rs index 116f078..b772282 100644 --- a/src/generator/builder.rs +++ b/src/generator/builder.rs @@ -4,8 +4,9 @@ use std::io::{Write, Cursor}; use zip::ZipWriter; use zip::write::FileOptions; use super::xml::*; +use super::theme_xml::*; use super::notes_xml::*; -use super::package_xml::{create_content_types_xml_with_notes, create_presentation_rels_xml_with_notes, create_slide_rels_xml_with_notes}; +use super::package_xml::{create_content_types_xml_with_notes, create_content_types_xml_with_charts, create_presentation_rels_xml_with_notes, create_slide_rels_xml_with_notes, create_slide_rels_xml_with_charts, create_slide_rels_xml_with_notes_and_charts}; /// Create a minimal but valid PPTX file pub fn create_pptx(title: &str, slides: usize) -> Result, Box> { @@ -44,14 +45,21 @@ fn write_package_files( slide_count: usize, custom_slides: Option<&Vec>, ) -> Result<(), Box> { - // Check if any slides have notes + // Check if any slides have notes or charts let has_notes = custom_slides .map(|slides| slides.iter().any(|s| s.notes.is_some())) .unwrap_or(false); + let has_charts = custom_slides + .map(|slides| slides.iter().any(|s| !s.charts.is_empty())) + .unwrap_or(false); + + - // 1. Content types (with notes if present) + // 1. Content types (with notes or charts if present) let content_types = if has_notes { create_content_types_xml_with_notes(slide_count, custom_slides) + } else if has_charts { + create_content_types_xml_with_charts(slide_count, custom_slides) } else { create_content_types_xml(slide_count) }; @@ -83,6 +91,9 @@ fn write_package_files( // 6. Slide relationships (with notes references if present) write_slide_relationships_with_notes(zip, options, custom_slides)?; + // 6.5. Charts (if charts present) + write_charts(zip, options, custom_slides)?; + // 7. Notes relationships (if notes present) if has_notes { write_notes_relationships(zip, options, custom_slides)?; @@ -98,15 +109,32 @@ fn write_package_files( zip.write_all(notes_master_rels.as_bytes())?; } - // 8. Slide layouts - let slide_layout = create_slide_layout_xml(); - zip.start_file("ppt/slideLayouts/slideLayout1.xml", *options)?; - zip.write_all(slide_layout.as_bytes())?; - - // 9. Layout relationships - let layout_rels = create_layout_rels_xml(); - zip.start_file("ppt/slideLayouts/_rels/slideLayout1.xml.rels", *options)?; - zip.write_all(layout_rels.as_bytes())?; + // 8. Slide layouts - generate all 11 layout types to match WPS + let layout_types = vec![ + "title", // 1. Title slide + "obj", // 2. Title and Content + "secHead", // 3. Section Header + "twoObj", // 4. Two Content + "twoTxTwoObj", // 5. Comparison + "titleOnly", // 6. Title Only + "blank", // 7. Blank + "objTx", // 8. Content with Caption + "picTx", // 9. Picture with Caption + "vertTx", // 10. Title and Vertical Text + "vertTitleAndTx", // 11. Vertical Title and Text + ]; + + for (i, layout_type) in layout_types.iter().enumerate() { + let layout_num = i + 1; + let slide_layout = create_slide_layout_xml_by_type(layout_type, layout_num); + zip.start_file(format!("ppt/slideLayouts/slideLayout{}.xml", layout_num), *options)?; + zip.write_all(slide_layout.as_bytes())?; + + // Create relationship file for each layout + let layout_rels = create_layout_rels_xml_for_layout(layout_num); + zip.start_file(format!("ppt/slideLayouts/_rels/slideLayout{}.xml.rels", layout_num), *options)?; + zip.write_all(layout_rels.as_bytes())?; + } // 10. Slide master let slide_master = create_slide_master_xml(); @@ -193,13 +221,24 @@ fn write_slide_relationships_with_notes( ) -> Result<(), Box> { match custom_slides { Some(slides) => { + let mut global_chart_counter = 0; // Initialize global chart counter for (i, slide) in slides.iter().enumerate() { let slide_num = i + 1; - let slide_rels = if slide.notes.is_some() { + let chart_count = slide.charts.len(); + + let slide_rels = if slide.notes.is_some() && chart_count > 0 { + create_slide_rels_xml_with_notes_and_charts(slide_num, chart_count, global_chart_counter) + } else if slide.notes.is_some() { create_slide_rels_xml_with_notes(slide_num) + } else if chart_count > 0 { + create_slide_rels_xml_with_charts(slide_num, chart_count, global_chart_counter) } else { create_slide_rels_xml() }; + + // Increment global chart counter after processing this slide + global_chart_counter += chart_count; + zip.start_file(format!("ppt/slides/_rels/slide{slide_num}.xml.rels"), *options)?; zip.write_all(slide_rels.as_bytes())?; } @@ -229,3 +268,52 @@ fn write_notes_relationships( } Ok(()) } + +/// Write chart XML files and Excel data files +fn write_charts( + zip: &mut ZipWriter>>, + options: &FileOptions, + custom_slides: Option<&Vec>, +) -> Result<(), Box> { + if let Some(slides) = custom_slides { + let mut chart_counter = 0; // Initialize chart counter for global chart numbering + for (i, slide) in slides.iter().enumerate() { + let slide_num = i + 1; + for (chart_index, chart) in slide.charts.iter().enumerate() { + // Calculate global chart number for standard naming (chart1.xml, chart2.xml, etc.) + let global_chart_num = chart_counter + chart_index + 1; + + // Write chart XML with standard naming + let chart_xml = crate::generator::charts::xml::generate_chart_data_xml(chart); + zip.start_file(format!("ppt/charts/chart{global_chart_num}.xml"), *options)?; + zip.write_all(chart_xml.as_bytes())?; + + // Write chart style XML with unique naming + let style_xml = crate::generator::charts::style::generate_chart_style_xml(chart); + zip.start_file(format!("ppt/charts/style{global_chart_num}.xml"), *options)?; + zip.write_all(style_xml.as_bytes())?; + + // Write chart colors XML with unique naming + let colors_xml = crate::generator::charts::style::generate_chart_colors_xml(chart); + zip.start_file(format!("ppt/charts/colors{global_chart_num}.xml"), *options)?; + zip.write_all(colors_xml.as_bytes())?; + + // Write chart relationship file with standard naming + let chart_relationship_xml = crate::generator::charts::create_chart_relationship_xml_with_styles( + global_chart_num, + &format!("chart{global_chart_num}_data.xlsx") + ); + zip.start_file(format!("ppt/charts/_rels/chart{global_chart_num}.xml.rels"), *options)?; + zip.write_all(chart_relationship_xml.as_bytes())?; + + // Write Excel data file (move to embeddings directory) + let excel_bytes = crate::generator::charts::excel::generate_excel_bytes(chart); + zip.start_file(format!("ppt/embeddings/chart{global_chart_num}_data.xlsx"), *options)?; + zip.write_all(&excel_bytes)?; + } + // Increment chart counter after processing all charts in this slide + chart_counter += slide.charts.len(); + } + } + Ok(()) +} diff --git a/src/generator/charts/builder.rs b/src/generator/charts/builder.rs index b493184..1a1260b 100644 --- a/src/generator/charts/builder.rs +++ b/src/generator/charts/builder.rs @@ -23,10 +23,10 @@ impl ChartBuilder { chart_type, categories: Vec::new(), series: Vec::new(), - x: 0, - y: 0, - width: 5000000, // Default width (5 inches in EMU) - height: 3750000, // Default height (3.75 inches in EMU) + x: 2675890, // Default x position (based on WPS reference) + y: 1725930, // Default y position (based on WPS reference) + width: 6839585, // Default width (based on WPS reference) + height: 3959860, // Default height (based on WPS reference) } } diff --git a/src/generator/charts/data.rs b/src/generator/charts/data.rs index c2df121..b9319eb 100644 --- a/src/generator/charts/data.rs +++ b/src/generator/charts/data.rs @@ -7,6 +7,8 @@ use super::types::ChartType; pub struct ChartSeries { pub name: String, pub values: Vec, + pub x_values: Option>, + pub bubble_sizes: Option>, } impl ChartSeries { @@ -15,6 +17,28 @@ impl ChartSeries { ChartSeries { name: name.to_string(), values, + x_values: None, + bubble_sizes: None, + } + } + + /// Create a new XY chart series (for scatter and bubble charts) + pub fn new_xy(name: &str, x_values: Vec, y_values: Vec) -> Self { + ChartSeries { + name: name.to_string(), + values: y_values, + x_values: Some(x_values), + bubble_sizes: None, + } + } + + /// Create a new bubble chart series + pub fn new_bubble(name: &str, x_values: Vec, y_values: Vec, bubble_sizes: Vec) -> Self { + ChartSeries { + name: name.to_string(), + values: y_values, + x_values: Some(x_values), + bubble_sizes: Some(bubble_sizes), } } diff --git a/src/generator/charts/excel.rs b/src/generator/charts/excel.rs new file mode 100644 index 0000000..5d6ebe2 --- /dev/null +++ b/src/generator/charts/excel.rs @@ -0,0 +1,377 @@ +use rust_xlsxwriter::Workbook; +use crate::generator::charts::data::Chart; +use crate::generator::charts::ChartType; + +/// Trait for Excel workbook writers that generate chart data +pub trait ExcelWriter { + /// Generate Excel workbook bytes for the chart data + fn generate_excel_bytes(&self, chart: &Chart) -> Vec; + + /// Get the Excel reference for categories + fn categories_ref(&self) -> String; + + /// Get the Excel reference for categories with specific range + fn categories_ref_with_range(&self, start_row: u32, end_row: u32) -> String; + + /// Get the Excel reference for series values + fn values_ref(&self, series_index: usize) -> String; + + /// Get the Excel reference for series values with specific range + fn values_ref_with_range(&self, series_index: usize, start_row: u32, end_row: u32) -> String; + + /// Get the Excel reference for series name + fn series_name_ref(&self, series_index: usize) -> String; + + /// Get the Excel reference for bubble sizes (for bubble charts) + fn bubble_sizes_ref(&self, series_index: usize) -> String { + // Default implementation - bubble charts should override this + let col = (series_index + 2) as u16; // Column C for bubble sizes + let col_letter = column_letter(col); + format!("{}!${col_letter}$2:${col_letter}$100", self.worksheet_name()) + } + + /// Get the Excel reference for bubble sizes with specific range (for bubble charts) + fn bubble_sizes_ref_with_range(&self, series_index: usize, start_row: u32, end_row: u32) -> String { + // Default implementation - bubble charts should override this + let col = (series_index + 2) as u16; // Column C for bubble sizes + let col_letter = column_letter(col); + format!("{}!${col_letter}${}:${col_letter}${}", self.worksheet_name(), start_row as usize, end_row as usize) + } + + /// Get the worksheet name for this chart (default: Sheet1) + fn worksheet_name(&self) -> String { + "Sheet1".to_string() + } +} + +/// Excel writer for category-based charts (bar, line, pie, area, etc.) +pub struct CategoryExcelWriter { + pub chart: Option<&'static Chart>, + pub worksheet_name: String, +} + +impl CategoryExcelWriter { + /// Create a new category Excel writer with default worksheet name + pub fn new() -> Self { + CategoryExcelWriter { + chart: None, + worksheet_name: "Sheet1".to_string(), + } + } + + /// Create a new category Excel writer with specific worksheet name + pub fn with_worksheet_name(name: String) -> Self { + CategoryExcelWriter { + chart: None, + worksheet_name: name, + } + } +} + +impl ExcelWriter for CategoryExcelWriter { + fn generate_excel_bytes(&self, chart: &Chart) -> Vec { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + // Write categories + if !chart.categories.is_empty() { + for (i, category) in chart.categories.iter().enumerate() { + worksheet.write_string((i + 1) as u32, 0, category).unwrap(); + } + } + + // Write series data + for (series_idx, series) in chart.series.iter().enumerate() { + let col = (series_idx + 1) as u16; + + // Write series name + worksheet.write_string(0, col, &series.name).unwrap(); + + // Write series values + for (i, value) in series.values.iter().enumerate() { + worksheet.write_number((i + 1) as u32, col, *value).unwrap(); + } + } + + // Get Excel bytes + workbook.save_to_buffer().unwrap() + } + + fn categories_ref(&self) -> String { + // For category charts, we need to know the actual data size + // This will be handled by the caller with proper context + format!("{}!$A$2:$A$100", self.worksheet_name()) + } + + fn categories_ref_with_range(&self, start_row: u32, end_row: u32) -> String { + format!("{}!$A${}:$A${}", self.worksheet_name(), start_row, end_row) + } + + fn worksheet_name(&self) -> String { + self.worksheet_name.clone() + } + + fn values_ref(&self, series_index: usize) -> String { + let col = (series_index + 2) as u16; // +2 because column A is categories, B is first series values + let col_letter = column_letter(col); + format!("{}!${col_letter}$2:${col_letter}$100", self.worksheet_name()) + } + + fn values_ref_with_range(&self, series_index: usize, start_row: u32, end_row: u32) -> String { + let col = (series_index + 2) as u16; // +2 because column A is categories, B is first series values + let col_letter = column_letter(col); + format!("{}!${col_letter}${}:${col_letter}${}", self.worksheet_name(), start_row as usize, end_row as usize) + } + + fn series_name_ref(&self, series_index: usize) -> String { + let col = (series_index + 2) as u16; // +2 because column A is categories, B is first series values + let col_letter = column_letter(col); + format!("{}!${col_letter}$1", self.worksheet_name()) + } +} + +/// Excel writer for XY charts (scatter, bubble) +pub struct XyExcelWriter { + pub worksheet_name: String, +} + +/// Excel writer for bubble charts +pub struct BubbleExcelWriter { + pub worksheet_name: String, +} + +impl XyExcelWriter { + /// Create a new XY Excel writer with default worksheet name + pub fn new() -> Self { + XyExcelWriter { + worksheet_name: "Sheet1".to_string(), + } + } + + /// Create a new XY Excel writer with specific worksheet name + pub fn with_worksheet_name(name: String) -> Self { + XyExcelWriter { + worksheet_name: name, + } + } +} + +impl ExcelWriter for XyExcelWriter { + fn generate_excel_bytes(&self, chart: &Chart) -> Vec { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + + for (series_idx, series) in chart.series.iter().enumerate() { + let offset = series_idx * 3; // 3 rows per series (header + data) + + // Write series name in column B header + worksheet.write_string(offset as u32, 1, &series.name).unwrap(); + + // Write X and Y values + if let Some(x_values) = &series.x_values { + for (i, (x, y)) in x_values.iter().zip(&series.values).enumerate() { + let row = (offset + i + 1) as u32; + worksheet.write_number(row, 0, *x).unwrap(); + worksheet.write_number(row, 1, *y).unwrap(); + } + } + } + + workbook.save_to_buffer().unwrap() + } + + fn categories_ref(&self) -> String { + format!("{}!$A$2:$A$100", self.worksheet_name()) // Default range + } + + fn categories_ref_with_range(&self, start_row: u32, end_row: u32) -> String { + format!("{}!$A${}:$A${}", self.worksheet_name(), start_row, end_row) + } + + fn worksheet_name(&self) -> String { + self.worksheet_name.clone() + } + + fn values_ref(&self, series_index: usize) -> String { + let offset = series_index * 3; + format!("{}!$B${}:$B${}", self.worksheet_name(), offset + 2, offset + 100) + } + + fn values_ref_with_range(&self, series_index: usize, start_row: u32, end_row: u32) -> String { + let offset = series_index * 3; + format!("{}!$B${}:$B${}", self.worksheet_name(), offset + start_row as usize, offset + end_row as usize) + } + + fn series_name_ref(&self, series_index: usize) -> String { + let offset = series_index * 3; + format!("{}!$B${}", self.worksheet_name(), offset + 1) + } +} + +impl BubbleExcelWriter { + /// Create a new bubble Excel writer with default worksheet name + pub fn new() -> Self { + BubbleExcelWriter { + worksheet_name: "Sheet1".to_string(), + } + } + + /// Create a new bubble Excel writer with specific worksheet name + pub fn with_worksheet_name(name: String) -> Self { + BubbleExcelWriter { + worksheet_name: name, + } + } +} + +impl ExcelWriter for BubbleExcelWriter { + fn generate_excel_bytes(&self, chart: &Chart) -> Vec { + generate_bubble_excel(chart) + } + + fn categories_ref(&self) -> String { + format!("{}!$A$2:$A$100", self.worksheet_name()) // Default range for X values + } + + fn categories_ref_with_range(&self, start_row: u32, end_row: u32) -> String { + format!("{}!$A${}:$A${}", self.worksheet_name(), start_row, end_row) + } + + fn values_ref(&self, series_index: usize) -> String { + let offset = series_index * 4; + format!("{}!$B${}:$B${}", self.worksheet_name(), offset + 2, offset + 100) + } + + fn values_ref_with_range(&self, series_index: usize, start_row: u32, end_row: u32) -> String { + let offset = series_index * 4; + format!("{}!$B${}:$B${}", self.worksheet_name(), offset + start_row as usize, offset + end_row as usize) + } + + fn series_name_ref(&self, series_index: usize) -> String { + let offset = series_index * 4; + format!("{}!$B${}", self.worksheet_name(), offset + 1) + } + + fn worksheet_name(&self) -> String { + self.worksheet_name.clone() + } +} + +/// Convert column number to Excel letter (1 -> A, 2 -> B, etc.) +fn column_letter(col_num: u16) -> String { + let mut result = String::new(); + let mut n = col_num; + + while n > 0 { + n -= 1; + result = format!("{}{}", ((n % 26) as u8 + b'A') as char, result); + n /= 26; + } + + result +} + +/// Generate Excel workbook bytes based on chart type +pub fn generate_excel_for_chart(chart: &Chart) -> Vec { + generate_excel_for_chart_with_name(chart, "Sheet1".to_string()) +} + +/// Generate Excel workbook bytes based on chart type with specific worksheet name +pub fn generate_excel_for_chart_with_name(chart: &Chart, worksheet_name: String) -> Vec { + match chart.chart_type { + ChartType::Scatter | ChartType::ScatterLines | ChartType::ScatterSmooth => { + let writer = XyExcelWriter::with_worksheet_name(worksheet_name); + writer.generate_excel_bytes(chart) + } + ChartType::Bubble => { + // For bubble charts, we need to include bubble sizes + generate_bubble_excel_with_name(chart, worksheet_name) + } + _ => { + // Category-based charts + let writer = CategoryExcelWriter::with_worksheet_name(worksheet_name); + writer.generate_excel_bytes(chart) + } + } +} + +/// Get the appropriate Excel writer for the chart type +pub fn get_excel_writer(chart_type: &ChartType) -> Box { + get_excel_writer_with_name(chart_type, "Sheet1".to_string()) +} + +/// Get the appropriate Excel writer for the chart type with specific worksheet name +pub fn get_excel_writer_with_name(chart_type: &ChartType, worksheet_name: String) -> Box { + match chart_type { + ChartType::Scatter | ChartType::ScatterLines | ChartType::ScatterSmooth => { + Box::new(XyExcelWriter::with_worksheet_name(worksheet_name)) + } + ChartType::Bubble => { + Box::new(BubbleExcelWriter::with_worksheet_name(worksheet_name)) + } + _ => { + // Category-based charts + Box::new(CategoryExcelWriter::with_worksheet_name(worksheet_name)) + } + } +} + +/// Generate Excel for bubble charts (includes bubble sizes) +fn generate_bubble_excel(chart: &Chart) -> Vec { + generate_bubble_excel_with_name(chart, "Sheet1".to_string()) +} + +/// Generate Excel for bubble charts with specific worksheet name +fn generate_bubble_excel_with_name(chart: &Chart, worksheet_name: String) -> Vec { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + worksheet.set_name(&worksheet_name).unwrap(); + + for (series_idx, series) in chart.series.iter().enumerate() { + let offset = series_idx * 4; // 4 rows per series (header + data) + + // Write series name in column B header + worksheet.write_string(offset as u32, 1, &series.name).unwrap(); + worksheet.write_string((offset + 1) as u32, 2, "Size").unwrap(); + + // Write X, Y, and bubble size values + if let Some(x_values) = &series.x_values { + for (i, (x, y)) in x_values.iter().zip(&series.values).enumerate() { + let row = (offset + i + 2) as u32; + worksheet.write_number(row, 0, *x).unwrap(); + worksheet.write_number(row, 1, *y).unwrap(); + + // Add bubble size if available + if let Some(bubble_sizes) = &series.bubble_sizes { + if let Some(size) = bubble_sizes.get(i) { + worksheet.write_number(row, 2, *size).unwrap(); + } + } + } + } + } + + workbook.save_to_buffer().unwrap() +} + +/// Generate worksheet name based on chart number +pub fn worksheet_name_for_chart(chart_number: usize) -> String { + if chart_number == 1 { + "Sheet1".to_string() + } else { + format!("Sheet{}", chart_number) + } +} + +/// Generate Excel bytes for a chart based on its type with specific chart number +pub fn generate_excel_bytes_for_chart(chart: &Chart, chart_number: usize) -> Vec { + let worksheet_name = worksheet_name_for_chart(chart_number); + let writer = get_excel_writer_with_name(&chart.chart_type, worksheet_name); + writer.generate_excel_bytes(chart) +} + +/// Generate Excel bytes for a chart based on its type (legacy function) +pub fn generate_excel_bytes(chart: &Chart) -> Vec { + let writer = get_excel_writer(&chart.chart_type); + writer.generate_excel_bytes(chart) +} \ No newline at end of file diff --git a/src/generator/charts/mod.rs b/src/generator/charts/mod.rs index 939c0bc..b22b047 100644 --- a/src/generator/charts/mod.rs +++ b/src/generator/charts/mod.rs @@ -9,12 +9,16 @@ mod types; mod data; mod builder; -mod xml; +pub mod xml; +pub mod excel; +pub mod relationships; +pub mod style; pub use types::ChartType; pub use data::{Chart, ChartSeries}; pub use builder::ChartBuilder; -pub use xml::generate_chart_xml; +pub use xml::{generate_chart_xml, generate_chart_xml_with_number}; +pub use relationships::*; /// Escape XML special characters pub(crate) fn escape_xml(s: &str) -> String { diff --git a/src/generator/charts/relationships.rs b/src/generator/charts/relationships.rs new file mode 100644 index 0000000..6299d71 --- /dev/null +++ b/src/generator/charts/relationships.rs @@ -0,0 +1,153 @@ +//! Chart relationship generation +//! +//! Generates chart relationship files that link charts to their Excel data sources + +/// Create chart relationship XML that links chart to Excel data source +pub fn create_chart_relationship_xml(_chart_number: usize, excel_filename: &str) -> String { + format!( + r#" + + +"#, + excel_filename + ) +} + +/// Create chart relationship XML with additional style and color references (like WPS does) +pub fn create_chart_relationship_xml_with_styles(chart_number: usize, excel_filename: &str) -> String { + format!( + r#" + + + + +"#, + excel_filename, chart_number, chart_number + ) +} + +/// Create minimal chart style XML +pub fn create_chart_style_xml(_chart_number: usize) -> String { + format!( + r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"# + ) +} + +/// Create minimal chart color style XML +pub fn create_chart_color_style_xml(_chart_number: usize) -> String { + format!( + r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"# + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_chart_relationship_xml() { + let xml = create_chart_relationship_xml(1, "Workbook1.xlsx"); + assert!(xml.contains("rId1")); + assert!(xml.contains("../embeddings/Workbook1.xlsx")); + assert!(xml.contains("http://schemas.openxmlformats.org/officeDocument/2006/relationships/package")); + } + + #[test] + fn test_create_chart_relationship_xml_with_styles() { + let xml = create_chart_relationship_xml_with_styles(1, "Workbook1.xlsx"); + assert!(xml.contains("rId1")); + assert!(xml.contains("rId2")); + assert!(xml.contains("rId3")); + assert!(xml.contains("../embeddings/Workbook1.xlsx")); + assert!(xml.contains("style1.xml")); + assert!(xml.contains("colors1.xml")); + } + + #[test] + fn test_create_chart_style_xml() { + let xml = create_chart_style_xml(1); + assert!(xml.contains("cs:chartStyle")); + assert!(xml.contains("chartArea")); + assert!(xml.contains("plotArea")); + } + + #[test] + fn test_create_chart_color_style_xml() { + let xml = create_chart_color_style_xml(1); + assert!(xml.contains("cs:colorStyle")); + assert!(xml.contains("variation")); + } +} \ No newline at end of file diff --git a/src/generator/charts/style.rs b/src/generator/charts/style.rs new file mode 100644 index 0000000..788840f --- /dev/null +++ b/src/generator/charts/style.rs @@ -0,0 +1,530 @@ +use crate::generator::charts::Chart; + +/// Generate chart style XML content +pub fn generate_chart_style_xml(_chart: &Chart) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Generate chart colors XML content +pub fn generate_chart_colors_xml(_chart: &Chart) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} \ No newline at end of file diff --git a/src/generator/charts/xml.rs b/src/generator/charts/xml.rs index 3ccae28..b1be1d4 100644 --- a/src/generator/charts/xml.rs +++ b/src/generator/charts/xml.rs @@ -3,28 +3,83 @@ use super::types::ChartType; use super::data::Chart; use super::escape_xml; +use super::excel::{get_excel_writer, get_excel_writer_with_name, worksheet_name_for_chart}; /// Generate chart XML for a slide pub fn generate_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_chart_xml_with_number(chart, shape_id, 1) +} + +/// Generate chart XML for a slide with specific chart number for worksheet naming +pub fn generate_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { match chart.chart_type { ChartType::Bar | ChartType::BarHorizontal | ChartType::BarStacked | ChartType::BarStacked100 => { - generate_bar_chart_xml(chart, shape_id) + generate_bar_chart_xml_with_number(chart, shape_id, chart_number) } ChartType::Line | ChartType::LineMarkers | ChartType::LineStacked => { - generate_line_chart_xml(chart, shape_id) + generate_line_chart_xml_with_number(chart, shape_id, chart_number) } - ChartType::Pie => generate_pie_chart_xml(chart, shape_id), + ChartType::Pie => generate_pie_chart_xml_with_number(chart, shape_id, chart_number), ChartType::Doughnut => generate_doughnut_chart_xml(chart, shape_id), ChartType::Area | ChartType::AreaStacked | ChartType::AreaStacked100 => { - generate_area_chart_xml(chart, shape_id) + generate_area_chart_xml_with_number(chart, shape_id, chart_number) + } + ChartType::Scatter | ChartType::ScatterLines | ChartType::ScatterSmooth => { + generate_scatter_chart_xml_with_number(chart, shape_id, chart_number) + } + ChartType::Bubble => generate_bubble_chart_xml_with_number(chart, shape_id, chart_number), + ChartType::Radar | ChartType::RadarFilled => generate_radar_chart_xml_with_number(chart, shape_id, chart_number), + ChartType::StockHLC | ChartType::StockOHLC => generate_stock_chart_xml_with_number(chart, shape_id, chart_number), + ChartType::Combo => generate_combo_chart_xml_with_number(chart, shape_id, chart_number), + } +} + +/// Generate chart frame reference for slide XML (only the frame with relationship reference) +/// This follows the python-pptx pattern where the slide contains only a reference to the chart data +pub fn generate_chart_frame_xml(chart: &Chart, shape_id: usize, relationship_id: &str) -> String { + format!( + r#" + + + + + + + + + + + + + + +"#, + shape_id, shape_id, chart.x, chart.y, chart.width, chart.height, relationship_id + ) +} + +/// Generate chart data XML for separate chart file (chart content without frame) +/// This follows the python-pptx pattern where chart data is stored in separate files +pub fn generate_chart_data_xml(chart: &Chart) -> String { + match chart.chart_type { + ChartType::Bar | ChartType::BarHorizontal | ChartType::BarStacked | ChartType::BarStacked100 => { + generate_bar_chart_data_xml(chart) + } + ChartType::Line | ChartType::LineMarkers | ChartType::LineStacked => { + generate_line_chart_data_xml(chart) + } + ChartType::Pie => generate_pie_chart_data_xml(chart), + ChartType::Doughnut => generate_doughnut_chart_data_xml(chart), + ChartType::Area | ChartType::AreaStacked | ChartType::AreaStacked100 => { + generate_area_chart_data_xml(chart) } ChartType::Scatter | ChartType::ScatterLines | ChartType::ScatterSmooth => { - generate_scatter_chart_xml(chart, shape_id) + generate_scatter_chart_data_xml(chart) } - ChartType::Bubble => generate_bubble_chart_xml(chart, shape_id), - ChartType::Radar | ChartType::RadarFilled => generate_radar_chart_xml(chart, shape_id), - ChartType::StockHLC | ChartType::StockOHLC => generate_stock_chart_xml(chart, shape_id), - ChartType::Combo => generate_combo_chart_xml(chart, shape_id), + ChartType::Bubble => generate_bubble_chart_data_xml(chart), + ChartType::Radar | ChartType::RadarFilled => generate_radar_chart_data_xml(chart), + ChartType::StockHLC | ChartType::StockOHLC => generate_stock_chart_data_xml(chart), + ChartType::Combo => generate_combo_chart_data_xml(chart), } } @@ -71,60 +126,112 @@ fn chart_frame_header(chart: &Chart, shape_id: usize) -> String { ) } -/// Generate the common chart frame footer -fn chart_frame_footer() -> &'static str { - r#" +/// Generate the common chart frame footer with external data reference +fn chart_frame_footer(relationship_id: Option<&str>) -> String { + let mut xml = String::from(r#" - +"#); + + // Add external data reference if provided + if let Some(rid) = relationship_id { + xml.push_str(&format!( + r#" + + +"#, + rid + )); + } + + xml.push_str(r#" -"# +"#); + + xml +} + +/// Generate series data XML using Excel references +fn generate_series_data(chart: &Chart, idx: usize, series_name: &str, values: &[f64]) -> String { + generate_series_data_with_number(chart, idx, series_name, values, 1) } -/// Generate series data XML -fn generate_series_data(_chart: &Chart, idx: usize, series_name: &str, values: &[f64]) -> String { +/// Generate series data XML using Excel references with specific chart number +fn generate_series_data_with_number(chart: &Chart, idx: usize, series_name: &str, values: &[f64], chart_number: usize) -> String { + + let worksheet_name = worksheet_name_for_chart(chart_number); + let excel_writer = get_excel_writer_with_name(&chart.chart_type, worksheet_name); + + // Calculate precise ranges based on actual data + let category_count = chart.categories.len(); + let start_row = 2; + let end_row = start_row + category_count as u32 - 1; + + let values_ref = excel_writer.values_ref_with_range(idx, start_row, end_row); + let name_ref = excel_writer.series_name_ref(idx); + let categories_ref = excel_writer.categories_ref_with_range(start_row, end_row); + + let mut xml = format!( r#" - - - - - - - -{} - - - + +{} + + + +{} + + + - - - - + + +{} + +"#, + idx, idx, name_ref, escape_xml(series_name), categories_ref, chart.category_count() + ); + + for (i, cat) in chart.categories.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, escape_xml(cat) + )); + } + + xml.push_str(&format!( + r#" + + + -Sheet1!$B${}:$B${} +{} -General"#, - idx, idx, escape_xml(series_name), 2 + idx, 2 + idx + values.len() - ); +0 +"#, + values_ref, values.len() + )); - for value in values { + for (i, value) in values.iter().enumerate() { xml.push_str(&format!( r#" - + {} "#, - value + i, value )); } @@ -139,8 +246,23 @@ fn generate_series_data(_chart: &Chart, idx: usize, series_name: &str, values: & xml } -/// Generate category axis XML +/// Generate category axis XML using Excel references fn generate_category_axis(chart: &Chart, ax_pos: &str) -> String { + generate_category_axis_with_number(chart, ax_pos, 1) +} + +/// Generate category axis XML using Excel references with specific chart number +fn generate_category_axis_with_number(chart: &Chart, ax_pos: &str, chart_number: usize) -> String { + let worksheet_name = worksheet_name_for_chart(chart_number); + let excel_writer = get_excel_writer_with_name(&chart.chart_type, worksheet_name); + + // Calculate precise ranges based on actual data + let data_count = chart.category_count(); + let start_row = 2; + let end_row = start_row + data_count as u32 - 1; + + let categories_ref = excel_writer.categories_ref_with_range(start_row, end_row); + let mut xml = format!( r#" @@ -151,15 +273,15 @@ fn generate_category_axis(chart: &Chart, ax_pos: &str) -> String { - + -Sheet1!$A$2:$A${} +{} "#, - ax_pos, 1 + chart.category_count(), chart.category_count() + ax_pos, categories_ref, chart.category_count() ); for (idx, cat) in chart.categories.iter().enumerate() { @@ -194,7 +316,7 @@ fn generate_value_axis(ax_pos: &str) -> String { - + @@ -205,6 +327,10 @@ fn generate_value_axis(ax_pos: &str) -> String { /// Generate bar chart XML fn generate_bar_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_bar_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_bar_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); xml.push_str(r#" @@ -212,38 +338,58 @@ fn generate_bar_chart_xml(chart: &Chart, shape_id: usize) -> String { "#); for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&generate_series_data(chart, idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_category_axis(chart, "l")); - xml.push_str(&generate_value_axis("b")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_with_number(chart, "l", chart_number)); + xml.push_str(&generate_value_axis("b")); + + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate line chart XML fn generate_line_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_line_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_line_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); xml.push_str(r#" "#); for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&generate_series_data(chart, idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_category_axis(chart, "b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_with_number(chart, "b", chart_number)); + xml.push_str(&generate_value_axis("l")); + + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } -/// Generate pie chart XML +/// Generate pie chart XML using Excel references fn generate_pie_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_pie_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_pie_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); xml.push_str(r#" @@ -251,89 +397,29 @@ fn generate_pie_chart_xml(chart: &Chart, shape_id: usize) -> String { // Pie chart uses first series only if let Some(series) = chart.series.first() { - xml.push_str(&format!( - r#" - - - - - - - - - - - -{} - - - - - + // Use the updated series data generation with precise ranges + xml.push_str(&generate_series_data_with_number(chart, 0, &series.name, &series.values, chart_number)); + + // Add pie-specific data labels + xml.push_str(r#" - - - -Sheet1!$B$2:$B${} - -General"#, - escape_xml(&series.name), - 1 + series.values.len() - )); - - for (idx, value) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - idx, value - )); - } - - xml.push_str(&format!( - r#" - - - - - -Sheet1!$A$2:$A${} - -"#, - 1 + chart.category_count(), - chart.category_count() - )); - - for (idx, cat) in chart.categories.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - idx, escape_xml(cat) - )); - } - - xml.push_str( - r#" - - - -"# - ); +"#); } xml.push_str(""); - xml.push_str(chart_frame_footer()); + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate doughnut chart XML fn generate_doughnut_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_doughnut_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_doughnut_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); xml.push_str(r#" @@ -342,84 +428,29 @@ fn generate_doughnut_chart_xml(chart: &Chart, shape_id: usize) -> String { // Doughnut chart uses first series only (like pie) if let Some(series) = chart.series.first() { - xml.push_str(&format!( - r#" - - - - - -Sheet1!$B$1 - - -{} - - - + // Use the updated series data generation with precise ranges + xml.push_str(&generate_series_data_with_number(chart, 0, &series.name, &series.values, chart_number)); + + // Add doughnut-specific data labels + xml.push_str(r#" - - - -Sheet1!$B$2:$B${} - -General"#, - escape_xml(&series.name), - 1 + series.values.len() - )); - - for (idx, value) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - idx, value - )); - } - - xml.push_str(&format!( - r#" - - - - - -Sheet1!$A$2:$A${} - -"#, - 1 + chart.category_count(), - chart.category_count() - )); - - for (idx, cat) in chart.categories.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - idx, escape_xml(cat) - )); - } - - xml.push_str( - r#" - - - -"# - ); +"#); } xml.push_str(""); - xml.push_str(chart_frame_footer()); + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate area chart XML fn generate_area_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_area_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_area_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); let grouping = chart.chart_type.grouping().unwrap_or("standard"); @@ -427,19 +458,29 @@ fn generate_area_chart_xml(chart: &Chart, shape_id: usize) -> String { "#, grouping)); for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&generate_series_data(chart, idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_category_axis(chart, "b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_with_number(chart, "b", chart_number)); + xml.push_str(&generate_value_axis("l")); + + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate scatter chart XML fn generate_scatter_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_scatter_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_scatter_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); let scatter_style = chart.chart_type.scatter_style().unwrap_or("lineMarker"); @@ -447,81 +488,28 @@ fn generate_scatter_chart_xml(chart: &Chart, shape_id: usize) -> String { "#, scatter_style)); for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&format!( - r#" - - - - - -Sheet1!$B$1 - - -{} - - - - - -Sheet1!$A$2:$A${} - -General"#, - idx, idx, escape_xml(&series.name), 1 + series.values.len() - )); - - // X values (use index as X) - for (i, _) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - i, i + 1 - )); - } - - xml.push_str(&format!( - r#" - - - - - -Sheet1!$B$2:$B${} - -General"#, - 1 + series.values.len() - )); - - for (i, value) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - i, value - )); - } - - xml.push_str( - r#" - - - -"# - ); + xml.push_str(&generate_series_data_for_scatter_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_value_axis("b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // X-axis (valAx) + xml.push_str(""); // Y-axis (valAx) xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart - ensure within plotArea but outside scatterChart + xml.push_str(&generate_value_axis_for_scatter_chart("b", 1, 2)); // X-axis, crossAx points to Y-axis + xml.push_str(&generate_value_axis_for_scatter_chart("l", 2, 1)); // Y-axis, crossAx points to X-axis + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate bubble chart XML fn generate_bubble_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_bubble_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_bubble_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); xml.push_str(r#" @@ -529,104 +517,29 @@ fn generate_bubble_chart_xml(chart: &Chart, shape_id: usize) -> String { "#); for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&format!( - r#" - - - - - -Sheet1!$B$1 - - -{} - - - - - -Sheet1!$A$2:$A${} - -General"#, - idx, idx, escape_xml(&series.name), 1 + series.values.len() - )); - - for (i, _) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - i, i + 1 - )); - } - - xml.push_str(&format!( - r#" - - - - - -Sheet1!$B$2:$B${} - -General"#, - 1 + series.values.len() - )); - - for (i, value) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - i, value - )); - } - - xml.push_str(&format!( - r#" - - - - - -Sheet1!$C$2:$C${} - -General"#, - 1 + series.values.len() - )); - - // Bubble sizes (use values as sizes) - for (i, value) in series.values.iter().enumerate() { - xml.push_str(&format!( - r#" - -{} -"#, - i, value.abs() - )); - } - - xml.push_str( - r#" - - - -"# - ); + xml.push_str(&generate_series_data_for_bubble_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_value_axis("b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart + xml.push_str(&generate_value_axis_for_chart("b", 2)); + xml.push_str(&generate_value_axis_for_chart("l", 3)); + + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate radar chart XML fn generate_radar_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_radar_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_radar_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); let radar_style = chart.chart_type.radar_style().unwrap_or("marker"); @@ -634,38 +547,58 @@ fn generate_radar_chart_xml(chart: &Chart, shape_id: usize) -> String { "#, radar_style)); for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&generate_series_data(chart, idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_category_axis(chart, "b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_with_number(chart, "b", chart_number)); + xml.push_str(&generate_value_axis("l")); + + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate stock chart XML fn generate_stock_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_stock_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_stock_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); xml.push_str(r#""#); // Stock charts need High, Low, Close (and optionally Open) series for (idx, series) in chart.series.iter().enumerate() { - xml.push_str(&generate_series_data(chart, idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_category_axis(chart, "b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); - xml.push_str(chart_frame_footer()); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_with_number(chart, "b", chart_number)); + xml.push_str(&generate_value_axis("l")); + + xml.push_str(&chart_frame_footer(Some("rId1"))); xml } /// Generate combo chart XML (bar + line) fn generate_combo_chart_xml(chart: &Chart, shape_id: usize) -> String { + generate_combo_chart_xml_with_number(chart, shape_id, 1) +} + +fn generate_combo_chart_xml_with_number(chart: &Chart, shape_id: usize, chart_number: usize) -> String { let mut xml = chart_frame_header(chart, shape_id); // First half of series as bars @@ -675,11 +608,12 @@ fn generate_combo_chart_xml(chart: &Chart, shape_id: usize) -> String { let mid = chart.series.len() / 2; for (idx, series) in chart.series.iter().take(mid.max(1)).enumerate() { - xml.push_str(&generate_series_data(chart, idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, chart_number)); } - xml.push_str(&generate_category_axis(chart, "b")); - xml.push_str(&generate_value_axis("l")); + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); // Second half as lines @@ -688,14 +622,1138 @@ fn generate_combo_chart_xml(chart: &Chart, shape_id: usize) -> String { "#); for (idx, series) in chart.series.iter().skip(mid.max(1)).enumerate() { - xml.push_str(&generate_series_data(chart, mid + idx, &series.name, &series.values)); + xml.push_str(&generate_series_data_with_number(chart, mid + idx, &series.name, &series.values, chart_number)); } + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx xml.push_str(""); } - xml.push_str(chart_frame_footer()); + // Axis definitions placed outside chart (shared by all charts) + xml.push_str(&generate_category_axis_with_number(chart, "b", chart_number)); + xml.push_str(&generate_value_axis("l")); + + xml.push_str(&chart_frame_footer(Some("rId1"))); + + xml +} + +/// Generate chart data XML header (common for all chart data files) +fn chart_data_header(chart: &Chart) -> String { + format!( + r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"# + ) +} + +/// Generate chart data XML footer (common for all chart data files) +fn chart_data_footer(relationship_id: Option<&str>) -> String { + let mut xml = String::from(r#" + + + + + + + + + + + +"#); + + // Add external data reference if provided + if let Some(rid) = relationship_id { + xml.push_str(&format!( + r#" + + +"#, + rid + )); + } + + // Add style and color references + xml.push_str(r#" + + + + + + + + + +"#); + + xml.push_str(r#" +"#); + + xml +} + +/// Generate bar chart data XML (for external chart file) +fn generate_bar_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + let bar_dir = if matches!(chart.chart_type, ChartType::BarHorizontal) { "bar" } else { "col" }; + let grouping = chart.chart_type.grouping().unwrap_or("clustered"); + + xml.push_str(&format!( + r#" + +"#, + bar_dir, grouping + )); + + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, 1)); + } + + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx + xml.push_str(""); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_for_chart(chart, "l")); + xml.push_str(&generate_value_axis_for_chart("b", 2)); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate line chart data XML (for external chart file) +fn generate_line_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + let grouping = chart.chart_type.grouping().unwrap_or("lineMarkers"); + + xml.push_str(&format!( + r#" +"#, + grouping + )); + + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, 1)); + } + + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx + xml.push_str(""); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_for_chart(chart, "b")); + xml.push_str(&generate_value_axis_for_chart("l", 2)); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate pie chart data XML (for external chart file) +fn generate_pie_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + xml.push_str(r#" +"#); + + // Pie chart uses first series only + if let Some(series) = chart.series.first() { + // Generate series with enhanced styling including individual data points + xml.push_str(&generate_pie_series_with_data_points(chart, 0, &series.name, &series.values)); + + // Add pie-specific data labels configuration (simplified like WPS) + xml.push_str(r#" + + +"#); + + // Add first slice angle + xml.push_str(r#" +"#); + } + + xml.push_str(""); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + // Add legend and other elements + xml.push_str(&chart_data_footer_with_legend(Some("rId1"))); + + xml +} + +/// Generate doughnut chart data XML (for external chart file) +fn generate_doughnut_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + xml.push_str(r#" + +"#); + + // Doughnut chart uses first series only (like pie) + if let Some(series) = chart.series.first() { + xml.push_str(&generate_series_data_with_number(chart, 0, &series.name, &series.values, 1)); + } + + xml.push_str(""); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate area chart data XML (for external chart file) +fn generate_area_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + let grouping = chart.chart_type.grouping().unwrap_or("standard"); + + xml.push_str(&format!( + r#" +"#, + grouping + )); + + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, 1)); + } + + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx + xml.push_str(""); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_for_chart(chart, "b")); + xml.push_str(&generate_value_axis_for_chart("l", 2)); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate scatter chart data XML (for external chart file) +fn generate_scatter_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + let scatter_style = chart.chart_type.scatter_style().unwrap_or("lineMarker"); + + xml.push_str(&format!( + r#" +"#, + scatter_style + )); + + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_for_scatter_with_number(chart, idx, &series.name, &series.values, 1)); + } + + // Add axis ID references (inside chart) + xml.push_str(""); // X-axis (valAx) + xml.push_str(""); // Y-axis (valAx) + xml.push_str(""); + + // Axis definitions placed outside chart - ensure within plotArea but outside scatterChart + xml.push_str(""); + xml.push_str(&format!(r#" + + + + + + + + + + +"#, 1)); + + xml.push_str(""); + xml.push_str(&format!(r#" + + + + + + + + + + +"#, 2)); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate bubble chart data XML (for external chart file) +fn generate_bubble_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + xml.push_str(r#" + +"#); + + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_for_bubble_with_number(chart, idx, &series.name, &series.values, 1)); + } + + // Add axis ID references (inside chart) + xml.push_str(""); // X-axis (valAx) + xml.push_str(""); // Y-axis (valAx) + xml.push_str(""); + + // Axis definitions placed outside chart + xml.push_str(&generate_value_axis_for_chart("b", 2)); + xml.push_str(&generate_value_axis_for_chart("l", 3)); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate radar chart data XML (for external chart file) +fn generate_radar_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + let radar_style = chart.chart_type.radar_style().unwrap_or("marker"); + + xml.push_str(&format!( + r#" +"#, + radar_style + )); + + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, 1)); + } + + // Add axis ID references (inside chart) + xml.push_str(""); // catAx + xml.push_str(""); // valAx + xml.push_str(""); + + // Axis definitions placed outside chart + xml.push_str(&generate_category_axis_for_chart(chart, "b")); + xml.push_str(&generate_value_axis_for_chart("l", 2)); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate stock chart data XML (for external chart file) +fn generate_stock_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + xml.push_str(r#""#); + + // Stock charts need High, Low, Close (and optionally Open) series + for (idx, series) in chart.series.iter().enumerate() { + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, 1)); + } + xml.push_str(&generate_category_axis_for_chart(chart, "b")); + xml.push_str(&generate_value_axis_for_chart("l", 2)); + xml.push_str(""); + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate combo chart data XML (for external chart file) +fn generate_combo_chart_data_xml(chart: &Chart) -> String { + let mut xml = chart_data_header(chart); + + // First half of series as bars + xml.push_str(r#" + +"#); + + let mid = chart.series.len() / 2; + for (idx, series) in chart.series.iter().take(mid.max(1)).enumerate() { + xml.push_str(&generate_series_data_with_number(chart, idx, &series.name, &series.values, 1)); + } + + xml.push_str(&generate_category_axis_for_chart(chart, "b")); + xml.push_str(&generate_value_axis_for_chart("l", 2)); + xml.push_str(""); + + // Second half as lines + if chart.series.len() > 1 { + xml.push_str(r#" +"#); + + for (idx, series) in chart.series.iter().skip(mid.max(1)).enumerate() { + xml.push_str(&generate_series_data_with_number(chart, mid + idx, &series.name, &series.values, 1)); + } + + xml.push_str(""); + } + + // Close plotArea before adding legend + xml.push_str(r#" +"#); + + xml.push_str(&chart_data_footer(Some("rId1"))); + + xml +} + +/// Generate pie series data XML with styling for chart data files +fn generate_pie_series_data_for_chart(chart: &Chart, idx: usize, series_name: &str, values: &[f64]) -> String { + let excel_writer = get_excel_writer(&chart.chart_type); + let name_ref = excel_writer.series_name_ref(idx); + let categories_ref = excel_writer.categories_ref(); + let values_ref = excel_writer.values_ref(idx); + + let mut xml = format!( + r#" + + + + + +{} + + + +{} + + + + + +"#, + idx, idx, name_ref, escape_xml(series_name) + ); + + // Add individual data point styling for pie chart slices + for (i, _) in values.iter().enumerate() { + let color_scheme = match i % 4 { + 0 => "accent1", + 1 => "accent2", + 2 => "accent3", + _ => "accent4", + }; + + xml.push_str(&format!( + r#" + + + + + + + + + + + + + + +"#, + i, color_scheme + )); + } + + // Add data labels for individual points + xml.push_str(r#" + + +"#); + + // Add category data + xml.push_str(&format!( + r#" + + +{} + +"#, + categories_ref, chart.category_count() + )); + + for (i, cat) in chart.categories.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, escape_xml(cat) + )); + } + + xml.push_str(&format!( + r#" + + + + + +{} + +General +"#, + values_ref, values.len() + )); + + for (i, value) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, value + )); + } + + xml.push_str( + r#" + + + +"# + ); + + xml +} + +/// Generate series data XML for chart data files +fn generate_series_data_for_chart(chart: &Chart, idx: usize, series_name: &str, values: &[f64]) -> String { + let excel_writer = get_excel_writer(&chart.chart_type); + let name_ref = excel_writer.series_name_ref(idx); + let categories_ref = excel_writer.categories_ref(); + let values_ref = excel_writer.values_ref(idx); + + let mut xml = format!( + r#" + + + + + +{} + + + +{} + + + +"#, + idx, idx, name_ref, escape_xml(series_name) + ); + + // Add category data + xml.push_str(&format!( + r#" + + +{} + +"#, + categories_ref, chart.category_count() + )); + + for (cat_idx, cat) in chart.categories.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + cat_idx, escape_xml(cat) + )); + } + + xml.push_str(&format!( + r#" + + + + + +{} + +0 +"#, + values_ref, values.len() + )); + + for (val_idx, value) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + val_idx, value + )); + } + + xml.push_str(&format!( + r#" + + + +"# + )); + + xml +} + +/// Generate series data XML for scatter charts +fn generate_series_data_for_scatter(_chart: &Chart, idx: usize, series_name: &str, values: &[f64]) -> String { + generate_series_data_for_scatter_with_number(_chart, idx, series_name, values, 1) +} + +/// Generate series data XML for scatter charts with specific chart number +fn generate_series_data_for_scatter_with_number(_chart: &Chart, idx: usize, series_name: &str, values: &[f64], chart_number: usize) -> String { + let worksheet_name = worksheet_name_for_chart(chart_number); + let excel_writer = get_excel_writer_with_name(&ChartType::Scatter, worksheet_name); + let name_ref = excel_writer.series_name_ref(idx); + + // Calculate precise ranges based on actual data + let data_count = values.len(); + let start_row = 2; + let end_row = start_row + data_count as u32 - 1; + + let categories_ref = excel_writer.categories_ref_with_range(start_row, end_row); + let values_ref = excel_writer.values_ref_with_range(idx, start_row, end_row); + + let mut xml = format!( + r#" + + + + + +{} + + + +{} + + + +"#, + idx, idx, name_ref, escape_xml(series_name) + ); + + // X values (use index as X) + xml.push_str(&format!( + r#" + + +{} + +0 +"#, + categories_ref, values.len() + )); + + for (i, _) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, i + 1 + )); + } + + xml.push_str(&format!( + r#" + + + + + +{} + +0 +"#, + values_ref, values.len() + )); + + for (i, value) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, value + )); + } + + xml.push_str(&format!( + r#" + + + + +"# + )); + + xml +} + +/// Generate series data XML for bubble charts +fn generate_series_data_for_bubble(_chart: &Chart, idx: usize, series_name: &str, values: &[f64]) -> String { + generate_series_data_for_bubble_with_number(_chart, idx, series_name, values, 1) +} + +/// Generate series data XML for bubble charts with specific chart number +fn generate_series_data_for_bubble_with_number(_chart: &Chart, idx: usize, series_name: &str, values: &[f64], chart_number: usize) -> String { + let worksheet_name = worksheet_name_for_chart(chart_number); + let excel_writer = get_excel_writer_with_name(&ChartType::Bubble, worksheet_name); + let name_ref = excel_writer.series_name_ref(idx); + + // Calculate precise ranges based on actual data + let data_count = values.len(); + let start_row = 2; + let end_row = start_row + data_count as u32 - 1; + + let categories_ref = excel_writer.categories_ref_with_range(start_row, end_row); + let values_ref = excel_writer.values_ref_with_range(idx, start_row, end_row); + let bubble_sizes_ref = excel_writer.bubble_sizes_ref_with_range(idx, start_row, end_row); + + + let mut xml = format!( + r#" + + + + + +{} + + + +{} + + + +"#, + idx, idx, name_ref, escape_xml(series_name) + ); + + // X values (use index as X) + xml.push_str(&format!( + r#" + + +{} + +0 +"#, + categories_ref, values.len() + )); + + for (i, _) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, i + 1 + )); + } + + xml.push_str(&format!( + r#" + + + + + +{} + +0 +"#, + values_ref, values.len() + )); + + for (i, value) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, value + )); + } + + xml.push_str(&format!( + r#" + + + + + +{} + +0 +"#, + bubble_sizes_ref, values.len() + )); + + // Bubble sizes (use values as sizes) + for (i, value) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, value.abs() + )); + } + + xml.push_str(&format!( + r#" + + + +"# + )); + + xml +} + +/// Generate category axis XML for chart data files +fn generate_category_axis_for_chart(_chart: &Chart, ax_pos: &str) -> String { + format!( + r#" + + + + + + + + + + + + +"#, + ax_pos + ) +} + +/// Generate value axis XML for chart data files +fn generate_value_axis_for_chart(ax_pos: &str, ax_id: i32) -> String { + format!( + r#" + + + + + + + + + + + + +"#, + ax_id, ax_pos + ) +} + +/// Generate value axis XML for scatter charts +fn generate_value_axis_for_scatter_chart(ax_pos: &str, ax_id: i32, cross_ax_id: i32) -> String { + format!( + r#" + + + + + + + + + + + + +"#, + ax_id, ax_pos, cross_ax_id + ) +} + +/// Generate pie series with individual data points styling +fn generate_pie_series_with_data_points(_chart: &Chart, series_idx: usize, series_name: &str, values: &[f64]) -> String { + let mut xml = String::new(); + + // Series header + xml.push_str(&format!( + r#" + + + + + +Sheet1!${}${} + + + +{} + + + + + +"#, + series_idx, series_idx, + (b'B' + series_idx as u8) as char, 1, // Column letter for series name + series_name + )); + + // Add individual data points with colors + for (i, _) in values.iter().enumerate() { + let color_idx = (i % 6) + 1; // Cycle through accent1-6 colors + xml.push_str(&format!( + r#" + + + + + + + + + + + + + + +"#, + i, color_idx + )); + } + + // Add categories and values + xml.push_str(&format!( + r#" + + + + + +Sheet1!$A$2:$A${} + +"#, + values.len() + 1, values.len() + )); + + for (i, _) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +Category {} +"#, + i, i + 1 + )); + } + + xml.push_str(&format!( + r#" + + + + + +Sheet1!${}$2:${}${} + +General +"#, + (b'B' + series_idx as u8) as char, + (b'B' + series_idx as u8) as char, values.len() + 1, + values.len() + )); + + for (i, value) in values.iter().enumerate() { + xml.push_str(&format!( + r#" + +{} +"#, + i, value + )); + } + + xml.push_str(r#" + + + +"#); + + xml +} + +/// Generate chart data footer with legend section +fn chart_data_footer_with_legend(rel_id: Option<&str>) -> String { + let mut xml = String::new(); + + // Add legend section (inside chart element) + xml.push_str(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#); + + // Close chart element + xml.push_str(r#" +"#); + + // Add chart styling (outside chart element, inside chartSpace) + xml.push_str(r#" + + + + + + + + + + + + + +"#); + + // Add external data reference + if let Some(id) = rel_id { + xml.push_str(&format!( + r#" + + +"#, + id + )); + } + + xml.push_str(r#" +"#); + xml } diff --git a/src/generator/layouts/blank.rs b/src/generator/layouts/blank.rs index cf28687..9bae07d 100644 --- a/src/generator/layouts/blank.rs +++ b/src/generator/layouts/blank.rs @@ -1,6 +1,8 @@ //! Blank slide layout use super::common::SlideXmlBuilder; +use crate::generator::slide_content::SlideContent; +use crate::generator::charts::xml::generate_chart_frame_xml; /// Blank slide layout generator pub struct BlankLayout; @@ -15,6 +17,26 @@ impl BlankLayout { .end_slide() .build() } + + /// Generate blank slide XML with chart support + pub fn generate_with_content(content: &SlideContent) -> String { + let mut builder = SlideXmlBuilder::new() + .start_slide_with_bg() + .start_sp_tree(); + + // Add charts + let chart_start_id = 2; + for (i, chart) in content.charts.iter().enumerate() { + let relationship_id = format!("rId{}", i + 2); // Start from rId2 (rId1 is usually for slide layout) + let chart_xml = generate_chart_frame_xml(chart, chart_start_id + i, &relationship_id); + builder = builder.raw("\n").raw(&chart_xml); + } + + builder + .end_sp_tree() + .end_slide() + .build() + } } #[cfg(test)] diff --git a/src/generator/layouts/centered_title.rs b/src/generator/layouts/centered_title.rs index 5c1e91e..c7dc481 100644 --- a/src/generator/layouts/centered_title.rs +++ b/src/generator/layouts/centered_title.rs @@ -2,6 +2,7 @@ use super::common::{SlideXmlBuilder, generate_text_props}; use crate::generator::slide_content::SlideContent; +use crate::generator::charts::xml::generate_chart_frame_xml; use crate::generator::constants::{ TITLE_X, CENTERED_TITLE_Y, TITLE_WIDTH, CENTERED_TITLE_HEIGHT, TITLE_FONT_SIZE, }; @@ -21,10 +22,20 @@ impl CenteredTitleLayout { content.title_color.as_deref(), ); - SlideXmlBuilder::new() + let mut builder = SlideXmlBuilder::new() .start_slide_with_bg() .start_sp_tree() - .add_centered_title(2, TITLE_X, CENTERED_TITLE_Y, TITLE_WIDTH, CENTERED_TITLE_HEIGHT, &content.title, &title_props) + .add_centered_title(2, TITLE_X, CENTERED_TITLE_Y, TITLE_WIDTH, CENTERED_TITLE_HEIGHT, &content.title, &title_props); + + // Add charts + let chart_start_id = 3; + for (i, chart) in content.charts.iter().enumerate() { + let relationship_id = format!("rId{}", i + 2); // Start from rId2 (rId1 is usually for slide layout) + let chart_xml = generate_chart_frame_xml(chart, chart_start_id + i, &relationship_id); + builder = builder.raw("\n").raw(&chart_xml); + } + + builder .end_sp_tree() .end_slide() .build() diff --git a/src/generator/layouts/title_content.rs b/src/generator/layouts/title_content.rs index 53f2310..b6c69b3 100644 --- a/src/generator/layouts/title_content.rs +++ b/src/generator/layouts/title_content.rs @@ -3,6 +3,7 @@ use super::common::{SlideXmlBuilder, generate_text_props, escape_xml}; use crate::generator::slide_content::SlideContent; use crate::generator::shapes_xml::generate_shape_xml; +use crate::generator::charts::xml::generate_chart_frame_xml; use crate::generator::constants::{ TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT, TITLE_HEIGHT_BIG, CONTENT_X, CONTENT_Y_START, CONTENT_Y_START_BIG, @@ -106,6 +107,14 @@ impl TitleContentLayout { )); } + // Add charts + let chart_start_id = 30 + content.shapes.len() + content.images.len(); + for (i, chart) in content.charts.iter().enumerate() { + let relationship_id = format!("rId{}", i + 2); // Start from rId2 (rId1 is usually for slide layout) + let chart_xml = generate_chart_frame_xml(chart, chart_start_id + i, &relationship_id); + builder = builder.raw("\n").raw(&chart_xml); + } + builder .end_sp_tree() .end_slide() @@ -157,6 +166,14 @@ impl TitleBigContentLayout { builder = builder.end_content_body(); } + // Add charts + let chart_start_id = 30 + content.shapes.len() + content.images.len(); + for (i, chart) in content.charts.iter().enumerate() { + let relationship_id = format!("rId{}", i + 2); // Start from rId2 (rId1 is usually for slide layout) + let chart_xml = generate_chart_frame_xml(chart, chart_start_id + i, &relationship_id); + builder = builder.raw("\n").raw(&chart_xml); + } + builder .end_sp_tree() .end_slide() diff --git a/src/generator/layouts/title_only.rs b/src/generator/layouts/title_only.rs index 71284e8..863b003 100644 --- a/src/generator/layouts/title_only.rs +++ b/src/generator/layouts/title_only.rs @@ -2,6 +2,7 @@ use super::common::{SlideXmlBuilder, generate_text_props}; use crate::generator::slide_content::SlideContent; +use crate::generator::charts::xml::generate_chart_frame_xml; use crate::generator::constants::{ TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT, TITLE_FONT_SIZE, }; @@ -21,10 +22,20 @@ impl TitleOnlyLayout { content.title_color.as_deref(), ); - SlideXmlBuilder::new() + let mut builder = SlideXmlBuilder::new() .start_slide_with_bg() .start_sp_tree() - .add_title(2, TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT, &content.title, &title_props, "title") + .add_title(2, TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT, &content.title, &title_props, "title"); + + // Add charts + let chart_start_id = 10; + for (i, chart) in content.charts.iter().enumerate() { + let relationship_id = format!("rId{}", i + 2); // Start from rId2 (rId1 is usually for slide layout) + let chart_xml = generate_chart_frame_xml(chart, chart_start_id + i, &relationship_id); + builder = builder.raw("\n").raw(&chart_xml); + } + + builder .end_sp_tree() .end_slide() .build() diff --git a/src/generator/layouts/two_column.rs b/src/generator/layouts/two_column.rs index 704abb2..40fef1f 100644 --- a/src/generator/layouts/two_column.rs +++ b/src/generator/layouts/two_column.rs @@ -2,6 +2,7 @@ use super::common::{SlideXmlBuilder, generate_text_props}; use crate::generator::slide_content::SlideContent; +use crate::generator::charts::xml::generate_chart_frame_xml; /// Two-column slide layout generator pub struct TwoColumnLayout; @@ -108,6 +109,14 @@ impl TwoColumnLayout { } } + // Add charts + let chart_start_id = 10; + for (i, chart) in content.charts.iter().enumerate() { + let relationship_id = format!("rId{}", i + 2); // Start from rId2 (rId1 is usually for slide layout) + let chart_xml = generate_chart_frame_xml(chart, chart_start_id + i, &relationship_id); + builder = builder.raw("\n").raw(&chart_xml); + } + builder .end_sp_tree() .end_slide() diff --git a/src/generator/package_xml.rs b/src/generator/package_xml.rs index 60faa16..904617a 100644 --- a/src/generator/package_xml.rs +++ b/src/generator/package_xml.rs @@ -15,7 +15,35 @@ pub fn create_content_types_xml(slides: usize) -> String { + + +"#.to_string(); + + for i in 1..=slides { + xml.push_str(&format!( + "\n" + )); + } + + xml.push_str(r#" + + + + + +"#); + xml +} + +/// Create [Content_Types].xml with chart support +pub fn create_content_types_xml_with_charts(slides: usize, custom_slides: Option<&Vec>) -> String { + let mut xml = r#" + + + + "#.to_string(); + for i in 1..=slides { xml.push_str(&format!( @@ -23,6 +51,34 @@ pub fn create_content_types_xml(slides: usize) -> String { )); } + // Add chart content types using global chart numbering + if let Some(slides_vec) = custom_slides { + let mut global_chart_counter = 0; // Initialize chart counter for global chart numbering + for (i, slide) in slides_vec.iter().enumerate() { + if !slide.charts.is_empty() { + let slide_num = i + 1; + for chart_index in 0..slide.charts.len() { + let global_chart_num = global_chart_counter + chart_index + 1; // Global chart number (1-based) + xml.push_str(&format!( + "\n" + )); + xml.push_str(&format!( + "\n" + )); + // Add chart style and color content types (using chart 1 for all charts as per WPS format) + xml.push_str(&format!( + "\n" + )); + xml.push_str(&format!( + "\n" + )); + } + // Increment chart counter after processing all charts in this slide + global_chart_counter += slide.charts.len(); + } + } + } + xml.push_str(r#" @@ -90,6 +146,8 @@ pub fn create_content_types_xml_with_notes(slides: usize, custom_slides: Option< + + "#.to_string(); for i in 1..=slides { @@ -156,3 +214,50 @@ pub fn create_slide_rels_xml_with_notes(slide_num: usize) -> String { "#) } + +/// Create slide relationship XML with chart references +pub fn create_slide_rels_xml_with_charts(_slide_num: usize, chart_count: usize, global_chart_counter: usize) -> String { + let mut xml = String::from(r#" + +"#); + + // Add chart relationships (chart XML files) using global chart numbering + for i in 0..chart_count { + let chart_id = i + 2; // Start from rId2 since rId1 is for layout + let global_chart_num = global_chart_counter + i + 1; // Global chart number (1-based) + xml.push_str(&format!( + r#" +"# + )); + } + + xml.push_str("\n"); + xml +} + +/// Create slide relationship XML with both notes and charts +pub fn create_slide_rels_xml_with_notes_and_charts(slide_num: usize, chart_count: usize, global_chart_counter: usize) -> String { + let mut xml = String::from(r#" + +"#); + + // Add chart relationships (chart XML files) using global chart numbering + for i in 0..chart_count { + let chart_id = i + 2; // Start from rId2 since rId1 is for layout + let global_chart_num = global_chart_counter + i + 1; // Global chart number (1-based) + xml.push_str(&format!( + r#" +"# + )); + } + + // Add notes slide relationship + let notes_id = chart_count + 2; // Continue from where chart IDs left off + xml.push_str(&format!( + r#" +"# + )); + + xml.push_str("\n"); + xml +} diff --git a/src/generator/slide_xml/mod.rs b/src/generator/slide_xml/mod.rs index f6a4f0a..7413c87 100644 --- a/src/generator/slide_xml/mod.rs +++ b/src/generator/slide_xml/mod.rs @@ -12,7 +12,8 @@ mod common; mod layouts; mod content; -use super::slide_content::{SlideContent, SlideLayout}; +use super::slide_content::SlideContent; +use crate::generator::layouts::create_slide_xml_for_layout; pub use common::create_slide_rels_xml; @@ -76,14 +77,7 @@ pub fn create_slide_xml(slide_num: usize, title: &str) -> String { /// Create slide XML with content based on layout pub fn create_slide_xml_with_content(_slide_num: usize, content: &SlideContent) -> String { - match content.layout { - SlideLayout::Blank => layouts::create_blank_slide(), - SlideLayout::TitleOnly => layouts::create_title_only_slide(content), - SlideLayout::CenteredTitle => layouts::create_centered_title_slide(content), - SlideLayout::TitleAndBigContent => layouts::create_title_and_big_content_slide(content), - SlideLayout::TwoColumn => layouts::create_two_column_slide(content), - SlideLayout::TitleAndContent => layouts::create_title_and_content_slide(content), - } + create_slide_xml_for_layout(content) } #[cfg(test)] diff --git a/src/generator/theme_xml.rs b/src/generator/theme_xml.rs index 5026fe2..4bfa00c 100644 --- a/src/generator/theme_xml.rs +++ b/src/generator/theme_xml.rs @@ -1,8 +1,224 @@ //! Theme, master, and layout XML generation +use crate::util::format_lang_attributes; +/// Create slide layout XML for different layout types +pub fn create_slide_layout_xml_by_type(layout_type: &str, layout_num: usize) -> String { + match layout_type { + "title" => create_title_slide_layout(layout_num), + "obj" => create_title_and_content_layout(layout_num), + "secHead" => create_section_header_layout(layout_num), + "twoObj" => create_two_content_layout(layout_num), + "twoTxTwoObj" => create_comparison_layout(layout_num), + "titleOnly" => create_title_only_layout(layout_num), + "blank" => create_blank_layout(layout_num), + "objTx" => create_content_with_caption_layout(layout_num), + "picTx" => create_picture_with_caption_layout(layout_num), + "vertTx" => create_title_and_vertical_text_layout(layout_num), + "vertTitleAndTx" => create_vertical_title_and_text_layout(layout_num), + _ => create_blank_layout(layout_num), + } +} -/// Create slide layout XML -pub fn create_slide_layout_xml() -> String { - r#" +/// Create title slide layout (layout 1) +pub fn create_title_slide_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +Click to edit Master title style + + + + + + + + + + + + + + + + + + + + + + +Click to edit Master subtitle style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create title and content layout (layout 2) +pub fn create_title_and_content_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +Click to edit Master title style + + + + + + + + + + + + +Click to edit Master text style +Second level +Third level +Fourth level +Fifth level + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create blank layout (layout 7) +pub fn create_blank_layout(_layout_num: usize) -> String { + format!(r#" @@ -24,7 +240,193 @@ pub fn create_slide_layout_xml() -> String { -"#.to_string() +"#) +} + +/// Create section header layout (layout 3) +pub fn create_section_header_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +Click to edit title + + + + + + + + + + + + + + + + + + + + + + +Click to edit text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create title only layout (layout 6) +pub fn create_title_only_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create slide layout XML (legacy function for backward compatibility) +pub fn create_slide_layout_xml() -> String { + create_blank_layout(1) } /// Create layout relationships XML @@ -35,6 +437,24 @@ pub fn create_layout_rels_xml() -> String { "#.to_string() } +/// Create layout relationships XML for a specific layout +pub fn create_layout_rels_xml_for_layout(layout_num: usize) -> String { + // Calculate starting tag number based on layout number + // Layout 1 uses tags 1-5, layout 2 uses tags 6-10, etc. + let start_tag = (layout_num - 1) * 5 + 1; + + format!(r#" + + + + + + + +"#, + start_tag, start_tag + 1, start_tag + 2, start_tag + 3, start_tag + 4) +} + /// Create slide master XML pub fn create_slide_master_xml() -> String { r#" @@ -64,6 +484,16 @@ pub fn create_slide_master_xml() -> String { + + + + + + + + + + "#.to_string() } @@ -73,10 +503,706 @@ pub fn create_master_rels_xml() -> String { r#" - + + + + + + + + + + + "#.to_string() } +/// Create two content layout (layout 4) +pub fn create_two_content_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + +Click to edit Master text style +Second level +Third level +第四级 +第五级 + + + + + + + + + + + + +Click to edit Master text style +Second level +Third level +第四级 +第五级 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create comparison layout (layout 5) +pub fn create_comparison_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + +Click to edit Master text style +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + +单击此处编辑母版文本样式 +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + +单击此处编辑母版文本样式 +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + +单击此处编辑母版文本样式 +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create content with caption layout (layout 8) +pub fn create_content_with_caption_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + +单击此处编辑母版文本样式 +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑标题 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create picture with caption layout (layout 9) +pub fn create_picture_with_caption_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + +单击图标添加图片 + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑标题 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create title and vertical text layout (layout 10) +pub fn create_title_and_vertical_text_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + +单击此处编辑母版文本样式 +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + +/// Create vertical title and text layout (layout 11) +pub fn create_vertical_title_and_text_layout(_layout_num: usize) -> String { + format!(r#" + + + + + + + + + + + + + + + + + + + + + + + + + + +单击此处编辑母版标题样式 + + + + + + + + + + + + +单击此处编辑母版文本样式 +第二级 +第三级 +第四级 +第五级 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#) +} + /// Create theme XML pub fn create_theme_xml() -> String { r#" diff --git a/src/generator/xml.rs b/src/generator/xml.rs index cac0fab..4358a13 100644 --- a/src/generator/xml.rs +++ b/src/generator/xml.rs @@ -18,7 +18,9 @@ pub use super::slide_xml::{ }; pub use super::theme_xml::{ create_slide_layout_xml, + create_slide_layout_xml_by_type, create_layout_rels_xml, + create_layout_rels_xml_for_layout, create_slide_master_xml, create_master_rels_xml, create_theme_xml, diff --git a/src/oxml/chart/mod.rs b/src/oxml/chart/mod.rs index b0b5c8e..c108927 100644 --- a/src/oxml/chart/mod.rs +++ b/src/oxml/chart/mod.rs @@ -3,6 +3,7 @@ //! Provides types for parsing and generating DrawingML chart elements. use super::xmlchemy::XmlElement; +use crate::util::format_lang_attributes; /// Chart type enumeration #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -100,7 +101,21 @@ impl NumericData { } pub fn from_values(values: &[f64]) -> Self { - let mut data = NumericData::new("Sheet1!$B$2"); + NumericData::from_values_with_sheet(values, "Sheet1") + } + + pub fn from_values_with_chart_number(values: &[f64], chart_number: usize) -> Self { + let sheet_name = if chart_number == 1 { + "Sheet1".to_string() + } else { + format!("Sheet{}", chart_number) + }; + NumericData::from_values_with_sheet(values, &sheet_name) + } + + pub fn from_values_with_sheet(values: &[f64], sheet_name: &str) -> Self { + let formula = format!("'{}'!$B$2", sheet_name); + let mut data = NumericData::new(&formula); for (i, &v) in values.iter().enumerate() { data.points.push(DataPoint::new(i as u32, v)); } @@ -137,7 +152,21 @@ impl StringData { } pub fn from_categories(categories: &[&str]) -> Self { - let mut data = StringData::new("Sheet1!$A$2"); + StringData::from_categories_with_sheet(categories, "Sheet1") + } + + pub fn from_categories_with_chart_number(categories: &[&str], chart_number: usize) -> Self { + let sheet_name = if chart_number == 1 { + "Sheet1".to_string() + } else { + format!("Sheet{}", chart_number) + }; + StringData::from_categories_with_sheet(categories, &sheet_name) + } + + pub fn from_categories_with_sheet(categories: &[&str], sheet_name: &str) -> Self { + let formula = format!("'{}'!$A$2", sheet_name); + let mut data = StringData::new(&formula); for (i, &cat) in categories.iter().enumerate() { data.points.push(CategoryPoint::new(i as u32, cat)); } @@ -183,6 +212,10 @@ impl ChartSeries { } pub fn parse(elem: &XmlElement) -> Option { + ChartSeries::parse_with_chart_number(elem, 1) + } + + pub fn parse_with_chart_number(elem: &XmlElement, chart_number: usize) -> Option { let index = elem.find("idx") .and_then(|e| e.attr("val")) .and_then(|v| v.parse().ok()) @@ -192,8 +225,13 @@ impl ChartSeries { .map(|t| t.text_content()) .unwrap_or_default(); - // Parse values - let values = NumericData::new("Sheet1!$B$2"); + // Parse values - use chart number for worksheet naming + let sheet_name = if chart_number == 1 { + "Sheet1".to_string() + } else { + format!("Sheet{}", chart_number) + }; + let values = NumericData::new(&format!("'{}'!$B$2", sheet_name)); Some(ChartSeries { index, @@ -204,10 +242,25 @@ impl ChartSeries { } pub fn to_xml(&self) -> String { + self.to_xml_with_sheet("Sheet1") + } + + pub fn to_xml_with_chart_number(&self, chart_number: usize) -> String { + let sheet_name = if chart_number == 1 { + "Sheet1".to_string() + } else { + format!("Sheet{}", chart_number) + }; + self.to_xml_with_sheet(&sheet_name) + } + + pub fn to_xml_with_sheet(&self, sheet_name: &str) -> String { + let formula = format!("'{}'!$B$1", sheet_name); let mut xml = format!( - r#"Sheet1!$B$1{}"#, + r#"{}{}"#, self.index, self.index, + formula, escape_xml(&self.name) ); @@ -318,8 +371,10 @@ impl ChartTitle { } pub fn to_xml(&self) -> String { + let lang_attrs = format_lang_attributes(); format!( - r#"{}"#, + r#"{}"#, + lang_attrs, escape_xml(&self.text) ) } diff --git a/src/oxml/editor.rs b/src/oxml/editor.rs index 56d0907..a3a3faa 100644 --- a/src/oxml/editor.rs +++ b/src/oxml/editor.rs @@ -307,6 +307,8 @@ impl PresentationEditor { + + {slide_overrides} diff --git a/src/oxml/table.rs b/src/oxml/table.rs index a347d5e..2a93a44 100644 --- a/src/oxml/table.rs +++ b/src/oxml/table.rs @@ -3,6 +3,7 @@ //! Provides types for parsing and generating DrawingML table elements. use super::xmlchemy::XmlElement; +use crate::util::format_lang_attributes; /// Table cell properties #[derive(Debug, Clone, Default)] @@ -98,10 +99,12 @@ impl TableCell { } let attr_str = if attrs.is_empty() { String::new() } else { format!(" {}", attrs.join(" ")) }; + let lang_attrs = format_lang_attributes(); format!( - r#"{}{}"#, + r#"{}{}"#, attr_str, + lang_attrs, escape_xml(&self.text), self.properties.to_xml() ) diff --git a/src/oxml/text.rs b/src/oxml/text.rs index 4ff0518..9104288 100644 --- a/src/oxml/text.rs +++ b/src/oxml/text.rs @@ -3,6 +3,7 @@ //! Provides types for parsing and generating DrawingML text elements. use super::xmlchemy::XmlElement; +use crate::util::format_lang_attributes; /// Text body properties (a:bodyPr) #[derive(Debug, Clone, Default)] @@ -155,7 +156,8 @@ impl RunProperties { } pub fn to_xml(&self) -> String { - let mut attrs = vec![r#"lang="en-US""#.to_string()]; + let lang_attrs = format_lang_attributes(); + let mut attrs = vec![lang_attrs]; if let Some(sz) = self.size { attrs.push(format!(r#"sz="{sz}""#)); diff --git a/src/parts/chart.rs b/src/parts/chart.rs index 6b340c9..94bbc1b 100644 --- a/src/parts/chart.rs +++ b/src/parts/chart.rs @@ -4,7 +4,7 @@ use super::base::{Part, PartType, ContentType}; use crate::exc::PptxError; -use crate::generator::charts::{Chart, generate_chart_xml}; +use crate::generator::charts::{Chart, generate_chart_xml_with_number}; /// Chart part (ppt/charts/chartN.xml) #[derive(Debug, Clone)] @@ -77,7 +77,7 @@ impl Part for ChartPart { } if let Some(ref chart) = self.chart { - return Ok(generate_chart_xml(chart, self.chart_number)); + return Ok(generate_chart_xml_with_number(chart, self.chart_number, self.chart_number)); } // Return minimal chart XML diff --git a/src/parts/notes_slide.rs b/src/parts/notes_slide.rs index 9a497f9..4e36658 100644 --- a/src/parts/notes_slide.rs +++ b/src/parts/notes_slide.rs @@ -5,6 +5,7 @@ use super::base::{Part, PartType, ContentType}; use crate::exc::PptxError; use crate::core::escape_xml; +use crate::util::format_lang_attributes; /// Notes slide part (ppt/notesSlides/notesSlideN.xml) #[derive(Debug, Clone)] @@ -62,14 +63,16 @@ impl NotesSlidePart { } fn generate_xml(&self) -> String { + let lang_attrs = format_lang_attributes(); let paragraphs: String = if self.notes_text.is_empty() { - "".to_string() + format!("", lang_attrs) } else { self.notes_text .lines() .map(|line| { format!( - "{}", + "{}", + lang_attrs, escape_xml(line) ) }) diff --git a/src/parts/slide.rs b/src/parts/slide.rs index e810dc8..0275a54 100644 --- a/src/parts/slide.rs +++ b/src/parts/slide.rs @@ -8,6 +8,7 @@ use crate::exc::PptxError; use crate::generator::SlideContent; use crate::generator::slide_xml::create_slide_xml_with_content; use crate::oxml::{SlideParser, ParsedSlide}; +use crate::generator::slide_content::SlideLayout; /// Slide part (ppt/slides/slideN.xml) #[derive(Debug, Clone)] @@ -17,6 +18,7 @@ pub struct SlidePart { content: Option, parsed: Option, xml_content: Option, + layout: SlideLayout, } impl SlidePart { @@ -28,17 +30,20 @@ impl SlidePart { content: None, parsed: None, xml_content: None, + layout: SlideLayout::TitleAndContent, } } /// Create from SlideContent pub fn from_content(slide_number: usize, content: SlideContent) -> Self { + let layout = content.layout; SlidePart { path: format!("ppt/slides/slide{}.xml", slide_number), slide_number, content: Some(content), parsed: None, xml_content: None, + layout, } } @@ -83,7 +88,15 @@ impl SlidePart { /// Create default relationships for slide pub fn create_relationships(&self) -> Relationships { let mut rels = Relationships::new(); - rels.add(RelationshipType::SlideLayout, "../slideLayouts/slideLayout1.xml"); + let layout_number = match self.layout { + crate::generator::slide_content::SlideLayout::TitleOnly => 1, + crate::generator::slide_content::SlideLayout::TitleAndContent => 2, + crate::generator::slide_content::SlideLayout::TitleAndBigContent => 3, + crate::generator::slide_content::SlideLayout::Blank => 4, + crate::generator::slide_content::SlideLayout::CenteredTitle => 5, + crate::generator::slide_content::SlideLayout::TwoColumn => 6, + }; + rels.add(RelationshipType::SlideLayout, &format!("../slideLayouts/slideLayout{}.xml", layout_number)); rels } @@ -149,6 +162,7 @@ impl Part for SlidePart { content: None, parsed: Some(parsed), xml_content: Some(xml.to_string()), + layout: SlideLayout::TitleAndContent, }) } } @@ -163,6 +177,7 @@ pub fn parse_slide(xml: &str, slide_number: usize) -> Result @@ -393,7 +395,7 @@ impl TableCellPart { - {}{} + {}{} {} @@ -402,6 +404,7 @@ impl TableCellPart { "#, attrs, p_align, + lang_attrs, rpr_attrs, color_xml, font_xml, diff --git a/src/util.rs b/src/util.rs index b5f2a24..ee083c6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -198,3 +198,127 @@ mod tests { assert_eq!(height.emu(), 6858000); } } + +/// Detects the system language and returns the appropriate language code +pub fn get_system_language() -> String { + // Try to get the system locale from environment variables + if let Ok(locale) = std::env::var("LANG") { + // Parse locale string like "en_US.UTF-8" or "zh_CN.UTF-8" + if locale.starts_with("zh_CN") { + return "zh-CN".to_string(); + } else if locale.starts_with("en_US") { + return "en-US".to_string(); + } else if locale.starts_with("en_GB") { + return "en-GB".to_string(); + } else if locale.starts_with("es") { + return "es-ES".to_string(); + } else if locale.starts_with("fr") { + return "fr-FR".to_string(); + } else if locale.starts_with("de") { + return "de-DE".to_string(); + } else if locale.starts_with("it") { + return "it-IT".to_string(); + } else if locale.starts_with("pt") { + return "pt-PT".to_string(); + } else if locale.starts_with("nl") { + return "nl-NL".to_string(); + } else if locale.starts_with("ru") { + return "ru-RU".to_string(); + } else if locale.starts_with("ja") { + return "ja-JP".to_string(); + } else if locale.starts_with("ko") { + return "ko-KR".to_string(); + } + } + + // Try LC_ALL environment variable + if let Ok(lc_all) = std::env::var("LC_ALL") { + if lc_all.starts_with("zh_CN") { + return "zh-CN".to_string(); + } else if lc_all.starts_with("en_US") { + return "en-US".to_string(); + } + } + + // Try Windows API on Windows systems + #[cfg(all(windows, feature = "windows-lang"))] + { + if let Ok(lang) = get_windows_language() { + return lang; + } + } + + // Default to en-US if we can't detect the language + "en-US".to_string() +} + +/// Get Windows system language using Windows API +#[cfg(all(windows, feature = "windows-lang"))] +fn get_windows_language() -> Result { + use windows_sys::Win32::System::SystemInformation::*; + + unsafe { + let mut buffer = [0u16; 256]; + let result = GetUserDefaultLocaleName(buffer.as_mut_ptr(), buffer.len() as i32); + + if result > 0 { + let locale = String::from_utf16_lossy(&buffer[..result as usize - 1]); + + // Convert Windows locale format to XML language format + match locale.as_str() { + "zh-CN" => Ok("zh-CN".to_string()), + "en-US" => Ok("en-US".to_string()), + "en-GB" => Ok("en-GB".to_string()), + "es-ES" => Ok("es-ES".to_string()), + "fr-FR" => Ok("fr-FR".to_string()), + "de-DE" => Ok("de-DE".to_string()), + "it-IT" => Ok("it-IT".to_string()), + "pt-PT" => Ok("pt-PT".to_string()), + "pt-BR" => Ok("pt-BR".to_string()), + "nl-NL" => Ok("nl-NL".to_string()), + "ru-RU" => Ok("ru-RU".to_string()), + "ja-JP" => Ok("ja-JP".to_string()), + "ko-KR" => Ok("ko-KR".to_string()), + _ => Ok("en-US".to_string()), + } + } else { + Err(()) + } + } +} + +/// Get the alternative language (usually English) for XML altLang attribute +pub fn get_alt_language() -> String { + let system_lang = get_system_language(); + + // If system language is already English, return empty string + if system_lang.starts_with("en") { + String::new() + } else { + "en-US".to_string() + } +} + +/// Format language attributes for XML +pub fn format_lang_attributes() -> String { + let lang = get_system_language(); + let alt_lang = get_alt_language(); + + if alt_lang.is_empty() { + format!(r#" lang="{}""#, lang) + } else { + format!(r#" lang="{}" altLang="{}""#, lang, alt_lang) + } +} + +/// Format language attributes for XML with additional attributes +pub fn format_lang_attributes_with(additional_attrs: &str) -> String { + let lang = get_system_language(); + let alt_lang = get_alt_language(); + + if alt_lang.is_empty() { + format!(r#" lang="{}"{}"#, lang, additional_attrs) + } else { + format!(r#" lang="{}" altLang="{}"{}"#, lang, alt_lang, additional_attrs) + } +} \ No newline at end of file