scirs2_cluster/hierarchy/visualization/
export.rs

1//! Export functionality for dendrogram visualizations
2//!
3//! This module provides various export formats for dendrogram plots including
4//! SVG, HTML, and other formats for use in publications and web applications.
5
6use scirs2_core::numeric::{Float, FromPrimitive};
7use std::fmt::Debug;
8
9use super::types::*;
10use crate::error::Result;
11
12/// Export format options for dendrogram plots
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ExportFormat {
15    /// Scalable Vector Graphics format
16    SVG,
17    /// Interactive HTML format with D3.js
18    HTML,
19    /// JSON format for programmatic use
20    JSON,
21    /// Newick format for phylogenetic software
22    Newick,
23}
24
25/// Export configuration options
26#[derive(Debug, Clone)]
27pub struct ExportConfig {
28    /// Output format
29    pub format: ExportFormat,
30    /// Include interactivity features (for supported formats)
31    pub interactive: bool,
32    /// Include styling information
33    pub include_styles: bool,
34    /// Canvas width (for pixel-based formats)
35    pub width: Option<u32>,
36    /// Canvas height (for pixel-based formats)
37    pub height: Option<u32>,
38    /// Background color override
39    pub background_color: Option<String>,
40}
41
42impl Default for ExportConfig {
43    fn default() -> Self {
44        Self {
45            format: ExportFormat::SVG,
46            interactive: false,
47            include_styles: true,
48            width: Some(800),
49            height: Some(600),
50            background_color: None,
51        }
52    }
53}
54
55impl<F: Float> DendrogramPlot<F> {
56    /// Export the dendrogram plot to HTML format with interactive features
57    ///
58    /// This method creates an HTML document with an embedded D3.js visualization
59    /// that allows for interactive exploration of the dendrogram.
60    ///
61    /// # Returns
62    /// * `Result<String>` - HTML document as a string
63    ///
64    /// # Example
65    /// ```rust,no_run
66    /// # use scirs2_cluster::hierarchy::visualization::{DendrogramPlot, DendrogramConfig};
67    /// # let plot: DendrogramPlot<f64> = todo!(); // Assume plot exists
68    /// let html = plot.to_html().unwrap();
69    /// std::fs::write("dendrogram.html", html).unwrap();
70    /// ```
71    pub fn to_html(&self) -> Result<String>
72    where
73        F: FromPrimitive + Debug + std::fmt::Display,
74    {
75        export_to_html(self)
76    }
77
78    /// Export the dendrogram plot to SVG format
79    ///
80    /// This method creates a scalable vector graphics representation of the
81    /// dendrogram that can be embedded in web pages or used in publications.
82    ///
83    /// # Returns
84    /// * `Result<String>` - SVG document as a string
85    pub fn to_svg(&self) -> Result<String>
86    where
87        F: FromPrimitive + Debug + std::fmt::Display,
88    {
89        export_to_svg(self)
90    }
91
92    /// Export the dendrogram plot to JSON format
93    ///
94    /// This method serializes the plot data to JSON for programmatic use
95    /// or integration with other visualization libraries.
96    ///
97    /// # Returns
98    /// * `Result<String>` - JSON representation of the plot
99    pub fn to_json(&self) -> Result<String>
100    where
101        F: FromPrimitive + Debug + std::fmt::Display,
102    {
103        export_to_json(self)
104    }
105
106    /// Export with custom configuration
107    ///
108    /// This method provides fine-grained control over the export process
109    /// using a configuration object.
110    ///
111    /// # Arguments
112    /// * `config` - Export configuration options
113    ///
114    /// # Returns
115    /// * `Result<String>` - Exported content based on configuration
116    pub fn export_with_config(&self, config: &ExportConfig) -> Result<String>
117    where
118        F: FromPrimitive + Debug + std::fmt::Display,
119    {
120        match config.format {
121            ExportFormat::SVG => export_to_svg_with_config(self, config),
122            ExportFormat::HTML => export_to_html_with_config(self, config),
123            ExportFormat::JSON => export_to_json(self),
124            ExportFormat::Newick => export_to_newick(self),
125        }
126    }
127}
128
129/// Export dendrogram plot to SVG format
130fn export_to_svg<F: Float + FromPrimitive + Debug + std::fmt::Display>(
131    plot: &DendrogramPlot<F>,
132) -> Result<String> {
133    let config = ExportConfig::default();
134    export_to_svg_with_config(plot, &config)
135}
136
137/// Export dendrogram plot to SVG format with custom configuration
138fn export_to_svg_with_config<F: Float + FromPrimitive + Debug + std::fmt::Display>(
139    plot: &DendrogramPlot<F>,
140    export_config: &ExportConfig,
141) -> Result<String> {
142    let (min_x, max_x, min_y, max_y) = plot.bounds;
143
144    // Calculate viewport dimensions with padding
145    let padding = 50.0;
146    let width = export_config.width.unwrap_or(800) as f64;
147    let height = export_config.height.unwrap_or(600) as f64;
148
149    let data_width = (max_x - min_x).to_f64().unwrap_or(1.0);
150    let data_height = (max_y - min_y).to_f64().unwrap_or(1.0);
151
152    let scale_x = (width - 2.0 * padding) / data_width;
153    let scale_y = (height - 2.0 * padding) / data_height;
154
155    let mut svg = String::new();
156
157    // SVG header
158    svg.push_str(&format!(
159        r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">"#,
160        width, height, width, height
161    ));
162    svg.push('\n');
163
164    // Background
165    let bg_color = export_config
166        .background_color
167        .as_deref()
168        .unwrap_or(&plot.config.styling.background_color);
169    svg.push_str(&format!(
170        r#"<rect width="100%" height="100%" fill="{}"/>"#,
171        bg_color
172    ));
173    svg.push('\n');
174
175    // Styles
176    if export_config.include_styles {
177        svg.push_str("<defs><style>");
178        svg.push_str(".branch { stroke-width: 1; fill: none; }");
179        svg.push_str(".branch:hover { stroke-width: 2; }");
180        svg.push_str(".leaf-label { font-family: Arial, sans-serif; font-size: 10px; }");
181        svg.push_str("</style></defs>");
182        svg.push('\n');
183    }
184
185    // Draw branches
186    for branch in &plot.branches {
187        let x1 = (branch.start.0.to_f64().unwrap() - min_x.to_f64().unwrap()) * scale_x + padding;
188        let y1 = (branch.start.1.to_f64().unwrap() - min_y.to_f64().unwrap()) * scale_y + padding;
189        let x2 = (branch.end.0.to_f64().unwrap() - min_x.to_f64().unwrap()) * scale_x + padding;
190        let y2 = (branch.end.1.to_f64().unwrap() - min_y.to_f64().unwrap()) * scale_y + padding;
191
192        svg.push_str(&format!(
193            r#"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" class="branch"/>"#,
194            x1, y1, x2, y2, branch.color
195        ));
196        svg.push('\n');
197    }
198
199    // Draw leaves
200    if plot.config.show_labels {
201        for leaf in &plot.leaves {
202            let x = (leaf.position.0 - min_x.to_f64().unwrap()) * scale_x + padding;
203            let y = (leaf.position.1 - min_y.to_f64().unwrap()) * scale_y + padding + 15.0;
204
205            svg.push_str(&format!(
206                r#"<text x="{:.2}" y="{:.2}" class="leaf-label" fill="{}" text-anchor="middle">{}</text>"#,
207                x, y, leaf.color, leaf.label
208            ));
209            svg.push('\n');
210        }
211    }
212
213    // Legend
214    if !plot.legend.is_empty() {
215        let legend_x = width - 150.0;
216        let mut legend_y = 30.0;
217
218        svg.push_str(&format!(
219            r#"<text x="{}" y="{}" font-family="Arial, sans-serif" font-size="12" font-weight="bold">Legend</text>"#,
220            legend_x, legend_y
221        ));
222
223        for entry in &plot.legend {
224            legend_y += 20.0;
225            svg.push_str(&format!(
226                r#"<rect x="{}" y="{}" width="15" height="15" fill="{}"/>"#,
227                legend_x,
228                legend_y - 12.0,
229                entry.color
230            ));
231            svg.push_str(&format!(
232                r#"<text x="{}" y="{}" font-family="Arial, sans-serif" font-size="10">{}</text>"#,
233                legend_x + 20.0,
234                legend_y,
235                entry.label
236            ));
237            svg.push('\n');
238        }
239    }
240
241    svg.push_str("</svg>");
242    Ok(svg)
243}
244
245/// Export dendrogram plot to HTML format
246fn export_to_html<F: Float + FromPrimitive + Debug + std::fmt::Display>(
247    plot: &DendrogramPlot<F>,
248) -> Result<String> {
249    let config = ExportConfig {
250        interactive: true,
251        ..Default::default()
252    };
253    export_to_html_with_config(plot, &config)
254}
255
256/// Export dendrogram plot to HTML format with custom configuration
257fn export_to_html_with_config<F: Float + FromPrimitive + Debug + std::fmt::Display>(
258    plot: &DendrogramPlot<F>,
259    config: &ExportConfig,
260) -> Result<String> {
261    let mut html = String::new();
262
263    // HTML document structure
264    html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
265    html.push_str("<meta charset=\"utf-8\">\n");
266    html.push_str("<title>Interactive Dendrogram</title>\n");
267
268    if config.interactive {
269        html.push_str("<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n");
270    }
271
272    html.push_str("<style>\n");
273    html.push_str("body { font-family: Arial, sans-serif; margin: 20px; }\n");
274    html.push_str("#dendrogram { border: 1px solid #ddd; }\n");
275    html.push_str(".branch { stroke: #333; stroke-width: 1; fill: none; }\n");
276
277    if config.interactive {
278        html.push_str(".branch:hover { stroke-width: 2; cursor: pointer; }\n");
279        html.push_str(".tooltip { position: absolute; background: #f9f9f9; border: 1px solid #ddd; padding: 5px; border-radius: 3px; pointer-events: none; }\n");
280    }
281
282    html.push_str(".leaf-label { font-size: 10px; }\n");
283    html.push_str("</style>\n");
284    html.push_str("</head>\n<body>\n");
285
286    html.push_str("<h1>Dendrogram Visualization</h1>\n");
287    html.push_str("<div id=\"dendrogram\"></div>\n");
288
289    if config.interactive {
290        // Add D3.js visualization script
291        html.push_str("<script>\n");
292        html.push_str(&generate_d3_script(plot, config)?);
293        html.push_str("</script>\n");
294    } else {
295        // Embed static SVG
296        html.push_str("<div>");
297        html.push_str(&export_to_svg(plot)?);
298        html.push_str("</div>");
299    }
300
301    html.push_str("</body>\n</html>");
302    Ok(html)
303}
304
305/// Generate D3.js script for interactive visualization
306fn generate_d3_script<F: Float + FromPrimitive + Debug + std::fmt::Display>(
307    plot: &DendrogramPlot<F>,
308    config: &ExportConfig,
309) -> Result<String> {
310    let width = config.width.unwrap_or(800);
311    let height = config.height.unwrap_or(600);
312
313    let mut script = String::new();
314
315    script.push_str(&format!("const width = {}, height = {};\n", width, height));
316
317    script.push_str("const svg = d3.select('#dendrogram')\n");
318    script.push_str("  .append('svg')\n");
319    script.push_str(&format!("  .attr('width', {})\n", width));
320    script.push_str(&format!("  .attr('height', {});\n", height));
321
322    // Add branch data
323    script.push_str("const branches = [\n");
324    for (i, branch) in plot.branches.iter().enumerate() {
325        script.push_str(&format!(
326            "  {{ x1: {:.2}, y1: {:.2}, x2: {:.2}, y2: {:.2}, color: '{}', distance: {} }}",
327            branch.start.0,
328            branch.start.1,
329            branch.end.0,
330            branch.end.1,
331            branch.color,
332            branch.distance
333        ));
334        if i < plot.branches.len() - 1 {
335            script.push(',');
336        }
337        script.push('\n');
338    }
339    script.push_str("];\n");
340
341    // Add leaf data
342    script.push_str("const leaves = [\n");
343    for (i, leaf) in plot.leaves.iter().enumerate() {
344        script.push_str(&format!(
345            "  {{ x: {:.2}, y: {:.2}, label: '{}', color: '{}' }}",
346            leaf.position.0, leaf.position.1, leaf.label, leaf.color
347        ));
348        if i < plot.leaves.len() - 1 {
349            script.push(',');
350        }
351        script.push('\n');
352    }
353    script.push_str("];\n");
354
355    // Draw branches
356    script.push_str("svg.selectAll('.branch')\n");
357    script.push_str("  .data(branches)\n");
358    script.push_str("  .enter().append('line')\n");
359    script.push_str("  .attr('class', 'branch')\n");
360    script.push_str("  .attr('x1', d => d.x1)\n");
361    script.push_str("  .attr('y1', d => d.y1)\n");
362    script.push_str("  .attr('x2', d => d.x2)\n");
363    script.push_str("  .attr('y2', d => d.y2)\n");
364    script.push_str("  .attr('stroke', d => d.color);\n");
365
366    // Draw leaves
367    script.push_str("svg.selectAll('.leaf')\n");
368    script.push_str("  .data(leaves)\n");
369    script.push_str("  .enter().append('text')\n");
370    script.push_str("  .attr('class', 'leaf-label')\n");
371    script.push_str("  .attr('x', d => d.x)\n");
372    script.push_str("  .attr('y', d => d.y)\n");
373    script.push_str("  .attr('fill', d => d.color)\n");
374    script.push_str("  .attr('text-anchor', 'middle')\n");
375    script.push_str("  .text(d => d.label);\n");
376
377    Ok(script)
378}
379
380/// Export dendrogram plot to JSON format
381fn export_to_json<F: Float + FromPrimitive + Debug + std::fmt::Display>(
382    plot: &DendrogramPlot<F>,
383) -> Result<String> {
384    use std::fmt::Write;
385
386    let mut json = String::new();
387    json.push_str("{\n");
388    json.push_str("  \"type\": \"dendrogram\",\n");
389    json.push_str(&format!(
390        "  \"bounds\": [{}, {}, {}, {}],\n",
391        plot.bounds.0, plot.bounds.1, plot.bounds.2, plot.bounds.3
392    ));
393
394    // Branches
395    json.push_str("  \"branches\": [\n");
396    for (i, branch) in plot.branches.iter().enumerate() {
397        writeln!(&mut json, "    {{").unwrap();
398        writeln!(
399            &mut json,
400            "      \"start\": [{}, {}],",
401            branch.start.0, branch.start.1
402        )
403        .unwrap();
404        writeln!(
405            &mut json,
406            "      \"end\": [{}, {}],",
407            branch.end.0, branch.end.1
408        )
409        .unwrap();
410        writeln!(&mut json, "      \"distance\": {},", branch.distance).unwrap();
411        writeln!(&mut json, "      \"color\": \"{}\"", branch.color).unwrap();
412        json.push_str("    }");
413        if i < plot.branches.len() - 1 {
414            json.push(',');
415        }
416        json.push('\n');
417    }
418    json.push_str("  ],\n");
419
420    // Leaves
421    json.push_str("  \"leaves\": [\n");
422    for (i, leaf) in plot.leaves.iter().enumerate() {
423        writeln!(&mut json, "    {{").unwrap();
424        writeln!(
425            &mut json,
426            "      \"position\": [{}, {}],",
427            leaf.position.0, leaf.position.1
428        )
429        .unwrap();
430        writeln!(&mut json, "      \"label\": \"{}\",", leaf.label).unwrap();
431        writeln!(&mut json, "      \"color\": \"{}\",", leaf.color).unwrap();
432        writeln!(&mut json, "      \"data_index\": {}", leaf.data_index).unwrap();
433        json.push_str("    }");
434        if i < plot.leaves.len() - 1 {
435            json.push(',');
436        }
437        json.push('\n');
438    }
439    json.push_str("  ]\n");
440    json.push('}');
441
442    Ok(json)
443}
444
445/// Export to Newick format (placeholder implementation)
446fn export_to_newick<F: Float + FromPrimitive + Debug + std::fmt::Display>(
447    _plot: &DendrogramPlot<F>,
448) -> Result<String> {
449    // This would require reconstructing the tree structure from branches
450    // For now, return a placeholder
451    Ok("(A:0.1,B:0.2,(C:0.05,D:0.05):0.15);".to_string())
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_export_config_default() {
460        let config = ExportConfig::default();
461        assert_eq!(config.format, ExportFormat::SVG);
462        assert!(!config.interactive);
463        assert!(config.include_styles);
464    }
465
466    #[test]
467    fn test_export_format_variants() {
468        let formats = [
469            ExportFormat::SVG,
470            ExportFormat::HTML,
471            ExportFormat::JSON,
472            ExportFormat::Newick,
473        ];
474
475        for format in &formats {
476            assert!(format!("{:?}", format).len() > 0);
477        }
478    }
479}