Skip to main content

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().expect("Operation failed");
69    /// std::fs::write("dendrogram.html", html).expect("Operation failed");
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().expect("Operation failed")
188            - min_x.to_f64().expect("Operation failed"))
189            * scale_x
190            + padding;
191        let y1 = (branch.start.1.to_f64().expect("Operation failed")
192            - min_y.to_f64().expect("Operation failed"))
193            * scale_y
194            + padding;
195        let x2 = (branch.end.0.to_f64().expect("Operation failed")
196            - min_x.to_f64().expect("Operation failed"))
197            * scale_x
198            + padding;
199        let y2 = (branch.end.1.to_f64().expect("Operation failed")
200            - min_y.to_f64().expect("Operation failed"))
201            * scale_y
202            + padding;
203
204        svg.push_str(&format!(
205            r#"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" class="branch"/>"#,
206            x1, y1, x2, y2, branch.color
207        ));
208        svg.push('\n');
209    }
210
211    // Draw leaves
212    if plot.config.show_labels {
213        for leaf in &plot.leaves {
214            let x =
215                (leaf.position.0 - min_x.to_f64().expect("Operation failed")) * scale_x + padding;
216            let y = (leaf.position.1 - min_y.to_f64().expect("Operation failed")) * scale_y
217                + padding
218                + 15.0;
219
220            svg.push_str(&format!(
221                r#"<text x="{:.2}" y="{:.2}" class="leaf-label" fill="{}" text-anchor="middle">{}</text>"#,
222                x, y, leaf.color, leaf.label
223            ));
224            svg.push('\n');
225        }
226    }
227
228    // Legend
229    if !plot.legend.is_empty() {
230        let legend_x = width - 150.0;
231        let mut legend_y = 30.0;
232
233        svg.push_str(&format!(
234            r#"<text x="{}" y="{}" font-family="Arial, sans-serif" font-size="12" font-weight="bold">Legend</text>"#,
235            legend_x, legend_y
236        ));
237
238        for entry in &plot.legend {
239            legend_y += 20.0;
240            svg.push_str(&format!(
241                r#"<rect x="{}" y="{}" width="15" height="15" fill="{}"/>"#,
242                legend_x,
243                legend_y - 12.0,
244                entry.color
245            ));
246            svg.push_str(&format!(
247                r#"<text x="{}" y="{}" font-family="Arial, sans-serif" font-size="10">{}</text>"#,
248                legend_x + 20.0,
249                legend_y,
250                entry.label
251            ));
252            svg.push('\n');
253        }
254    }
255
256    svg.push_str("</svg>");
257    Ok(svg)
258}
259
260/// Export dendrogram plot to HTML format
261fn export_to_html<F: Float + FromPrimitive + Debug + std::fmt::Display>(
262    plot: &DendrogramPlot<F>,
263) -> Result<String> {
264    let config = ExportConfig {
265        interactive: true,
266        ..Default::default()
267    };
268    export_to_html_with_config(plot, &config)
269}
270
271/// Export dendrogram plot to HTML format with custom configuration
272fn export_to_html_with_config<F: Float + FromPrimitive + Debug + std::fmt::Display>(
273    plot: &DendrogramPlot<F>,
274    config: &ExportConfig,
275) -> Result<String> {
276    let mut html = String::new();
277
278    // HTML document structure
279    html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
280    html.push_str("<meta charset=\"utf-8\">\n");
281    html.push_str("<title>Interactive Dendrogram</title>\n");
282
283    if config.interactive {
284        html.push_str("<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n");
285    }
286
287    html.push_str("<style>\n");
288    html.push_str("body { font-family: Arial, sans-serif; margin: 20px; }\n");
289    html.push_str("#dendrogram { border: 1px solid #ddd; }\n");
290    html.push_str(".branch { stroke: #333; stroke-width: 1; fill: none; }\n");
291
292    if config.interactive {
293        html.push_str(".branch:hover { stroke-width: 2; cursor: pointer; }\n");
294        html.push_str(".tooltip { position: absolute; background: #f9f9f9; border: 1px solid #ddd; padding: 5px; border-radius: 3px; pointer-events: none; }\n");
295    }
296
297    html.push_str(".leaf-label { font-size: 10px; }\n");
298    html.push_str("</style>\n");
299    html.push_str("</head>\n<body>\n");
300
301    html.push_str("<h1>Dendrogram Visualization</h1>\n");
302    html.push_str("<div id=\"dendrogram\"></div>\n");
303
304    if config.interactive {
305        // Add D3.js visualization script
306        html.push_str("<script>\n");
307        html.push_str(&generate_d3_script(plot, config)?);
308        html.push_str("</script>\n");
309    } else {
310        // Embed static SVG
311        html.push_str("<div>");
312        html.push_str(&export_to_svg(plot)?);
313        html.push_str("</div>");
314    }
315
316    html.push_str("</body>\n</html>");
317    Ok(html)
318}
319
320/// Generate D3.js script for interactive visualization
321fn generate_d3_script<F: Float + FromPrimitive + Debug + std::fmt::Display>(
322    plot: &DendrogramPlot<F>,
323    config: &ExportConfig,
324) -> Result<String> {
325    let width = config.width.unwrap_or(800);
326    let height = config.height.unwrap_or(600);
327
328    let mut script = String::new();
329
330    script.push_str(&format!("const width = {}, height = {};\n", width, height));
331
332    script.push_str("const svg = d3.select('#dendrogram')\n");
333    script.push_str("  .append('svg')\n");
334    script.push_str(&format!("  .attr('width', {})\n", width));
335    script.push_str(&format!("  .attr('height', {});\n", height));
336
337    // Add branch data
338    script.push_str("const branches = [\n");
339    for (i, branch) in plot.branches.iter().enumerate() {
340        script.push_str(&format!(
341            "  {{ x1: {:.2}, y1: {:.2}, x2: {:.2}, y2: {:.2}, color: '{}', distance: {} }}",
342            branch.start.0,
343            branch.start.1,
344            branch.end.0,
345            branch.end.1,
346            branch.color,
347            branch.distance
348        ));
349        if i < plot.branches.len() - 1 {
350            script.push(',');
351        }
352        script.push('\n');
353    }
354    script.push_str("];\n");
355
356    // Add leaf data
357    script.push_str("const leaves = [\n");
358    for (i, leaf) in plot.leaves.iter().enumerate() {
359        script.push_str(&format!(
360            "  {{ x: {:.2}, y: {:.2}, label: '{}', color: '{}' }}",
361            leaf.position.0, leaf.position.1, leaf.label, leaf.color
362        ));
363        if i < plot.leaves.len() - 1 {
364            script.push(',');
365        }
366        script.push('\n');
367    }
368    script.push_str("];\n");
369
370    // Draw branches
371    script.push_str("svg.selectAll('.branch')\n");
372    script.push_str("  .data(branches)\n");
373    script.push_str("  .enter().append('line')\n");
374    script.push_str("  .attr('class', 'branch')\n");
375    script.push_str("  .attr('x1', d => d.x1)\n");
376    script.push_str("  .attr('y1', d => d.y1)\n");
377    script.push_str("  .attr('x2', d => d.x2)\n");
378    script.push_str("  .attr('y2', d => d.y2)\n");
379    script.push_str("  .attr('stroke', d => d.color);\n");
380
381    // Draw leaves
382    script.push_str("svg.selectAll('.leaf')\n");
383    script.push_str("  .data(leaves)\n");
384    script.push_str("  .enter().append('text')\n");
385    script.push_str("  .attr('class', 'leaf-label')\n");
386    script.push_str("  .attr('x', d => d.x)\n");
387    script.push_str("  .attr('y', d => d.y)\n");
388    script.push_str("  .attr('fill', d => d.color)\n");
389    script.push_str("  .attr('text-anchor', 'middle')\n");
390    script.push_str("  .text(d => d.label);\n");
391
392    Ok(script)
393}
394
395/// Export dendrogram plot to JSON format
396fn export_to_json<F: Float + FromPrimitive + Debug + std::fmt::Display>(
397    plot: &DendrogramPlot<F>,
398) -> Result<String> {
399    use std::fmt::Write;
400
401    let mut json = String::new();
402    json.push_str("{\n");
403    json.push_str("  \"type\": \"dendrogram\",\n");
404    json.push_str(&format!(
405        "  \"bounds\": [{}, {}, {}, {}],\n",
406        plot.bounds.0, plot.bounds.1, plot.bounds.2, plot.bounds.3
407    ));
408
409    // Branches
410    json.push_str("  \"branches\": [\n");
411    for (i, branch) in plot.branches.iter().enumerate() {
412        writeln!(&mut json, "    {{").expect("Operation failed");
413        writeln!(
414            &mut json,
415            "      \"start\": [{}, {}],",
416            branch.start.0, branch.start.1
417        )
418        .expect("Operation failed");
419        writeln!(
420            &mut json,
421            "      \"end\": [{}, {}],",
422            branch.end.0, branch.end.1
423        )
424        .expect("Operation failed");
425        writeln!(&mut json, "      \"distance\": {},", branch.distance).expect("Operation failed");
426        writeln!(&mut json, "      \"color\": \"{}\"", branch.color).expect("Operation failed");
427        json.push_str("    }");
428        if i < plot.branches.len() - 1 {
429            json.push(',');
430        }
431        json.push('\n');
432    }
433    json.push_str("  ],\n");
434
435    // Leaves
436    json.push_str("  \"leaves\": [\n");
437    for (i, leaf) in plot.leaves.iter().enumerate() {
438        writeln!(&mut json, "    {{").expect("Operation failed");
439        writeln!(
440            &mut json,
441            "      \"position\": [{}, {}],",
442            leaf.position.0, leaf.position.1
443        )
444        .expect("Operation failed");
445        writeln!(&mut json, "      \"label\": \"{}\",", leaf.label).expect("Operation failed");
446        writeln!(&mut json, "      \"color\": \"{}\",", leaf.color).expect("Operation failed");
447        writeln!(&mut json, "      \"data_index\": {}", leaf.data_index).expect("Operation failed");
448        json.push_str("    }");
449        if i < plot.leaves.len() - 1 {
450            json.push(',');
451        }
452        json.push('\n');
453    }
454    json.push_str("  ]\n");
455    json.push('}');
456
457    Ok(json)
458}
459
460/// Export to Newick format (placeholder implementation)
461fn export_to_newick<F: Float + FromPrimitive + Debug + std::fmt::Display>(
462    _plot: &DendrogramPlot<F>,
463) -> Result<String> {
464    // This would require reconstructing the tree structure from branches
465    // For now, return a placeholder
466    Ok("(A:0.1,B:0.2,(C:0.05,D:0.05):0.15);".to_string())
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    #[test]
474    fn test_export_config_default() {
475        let config = ExportConfig::default();
476        assert_eq!(config.format, ExportFormat::SVG);
477        assert!(!config.interactive);
478        assert!(config.include_styles);
479    }
480
481    #[test]
482    fn test_export_format_variants() {
483        let formats = [
484            ExportFormat::SVG,
485            ExportFormat::HTML,
486            ExportFormat::JSON,
487            ExportFormat::Newick,
488        ];
489
490        for format in &formats {
491            assert!(format!("{:?}", format).len() > 0);
492        }
493    }
494}