1use scirs2_core::numeric::{Float, FromPrimitive};
7use std::fmt::Debug;
8
9use super::types::*;
10use crate::error::Result;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ExportFormat {
15 SVG,
17 HTML,
19 JSON,
21 Newick,
23}
24
25#[derive(Debug, Clone)]
27pub struct ExportConfig {
28 pub format: ExportFormat,
30 pub interactive: bool,
32 pub include_styles: bool,
34 pub width: Option<u32>,
36 pub height: Option<u32>,
38 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 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 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 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 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
129fn 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
137fn 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 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.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 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 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 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 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 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
260fn 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
271fn 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.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 html.push_str("<script>\n");
307 html.push_str(&generate_d3_script(plot, config)?);
308 html.push_str("</script>\n");
309 } else {
310 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
320fn 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 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 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 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 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
395fn 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 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 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
460fn export_to_newick<F: Float + FromPrimitive + Debug + std::fmt::Display>(
462 _plot: &DendrogramPlot<F>,
463) -> Result<String> {
464 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}