use chrono::{DateTime, Utc};
use scirs2_core::ndarray::{Array1, Array2};
use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use super::animation::{AnimationFrame, StreamingFrame};
use super::interactive::{CameraState, ClusterStats, ViewMode};
use super::{ScatterPlot2D, ScatterPlot3D, VisualizationConfig};
use crate::error::{ClusteringError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExportFormat {
PNG,
SVG,
PDF,
GIF,
MP4,
WebM,
HTML,
JSON,
CSV,
PlotlyJSON,
ThreeJS,
GLTF,
Unity3D,
Blender,
RGGplot,
Matplotlib,
D3JS,
WebGL,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
pub format: ExportFormat,
pub dimensions: (u32, u32),
pub dpi: u32,
pub quality: u8,
pub fps: f32,
pub duration: f32,
pub include_metadata: bool,
pub compression: u8,
pub background_color: String,
pub interactive: bool,
pub custom_styling: Option<String>,
pub animation_controls: bool,
pub stereoscopic: bool,
}
impl Default for ExportConfig {
fn default() -> Self {
Self {
format: ExportFormat::PNG,
dimensions: (1920, 1080),
dpi: 300,
quality: 90,
fps: 30.0,
duration: 10.0,
include_metadata: true,
compression: 6,
background_color: "#FFFFFF".to_string(),
interactive: false,
custom_styling: None,
animation_controls: true,
stereoscopic: false,
}
}
}
#[allow(dead_code)]
pub fn export_scatter_2d_to_file<P: AsRef<Path>>(
plot: &ScatterPlot2D,
output_path: P,
config: &ExportConfig,
) -> Result<()> {
let path = output_path.as_ref();
match config.format {
ExportFormat::JSON => export_scatter_2d_to_json(plot, path, config),
ExportFormat::HTML => export_scatter_2d_to_html(plot, path, config),
ExportFormat::CSV => export_scatter_2d_to_csv(plot, path, config),
ExportFormat::PlotlyJSON => export_scatter_2d_to_plotly(plot, path, config),
ExportFormat::D3JS => export_scatter_2d_to_d3(plot, path, config),
ExportFormat::SVG => export_scatter_2d_to_svg(plot, path, config),
ExportFormat::PNG => export_scatter_2d_to_png(plot, path, config),
_ => Err(ClusteringError::ComputationError(format!(
"Unsupported export format {:?} for 2D scatter plot",
config.format
))),
}
}
#[allow(dead_code)]
pub fn export_scatter_3d_to_file<P: AsRef<Path>>(
plot: &ScatterPlot3D,
output_path: P,
config: &ExportConfig,
) -> Result<()> {
let path = output_path.as_ref();
match config.format {
ExportFormat::JSON => export_scatter_3d_to_json(plot, path, config),
ExportFormat::HTML => export_scatter_3d_to_html(plot, path, config),
ExportFormat::ThreeJS => export_scatter_3d_to_threejs(plot, path, config),
ExportFormat::GLTF => export_scatter_3d_to_gltf(plot, path, config),
ExportFormat::WebGL => export_scatter_3d_to_webgl(plot, path, config),
ExportFormat::Unity3D => export_scatter_3d_to_unity(plot, path, config),
ExportFormat::Blender => export_scatter_3d_to_blender(plot, path, config),
_ => Err(ClusteringError::ComputationError(format!(
"Unsupported export format {:?} for 3D scatter plot",
config.format
))),
}
}
#[allow(dead_code)]
pub fn export_animation_to_file<P: AsRef<Path>>(
frames: &[AnimationFrame],
output_path: P,
config: &ExportConfig,
) -> Result<()> {
let path = output_path.as_ref();
match config.format {
ExportFormat::GIF => export_animation_to_gif(frames, path, config),
ExportFormat::MP4 => export_animation_to_mp4(frames, path, config),
ExportFormat::WebM => export_animation_to_webm(frames, path, config),
ExportFormat::HTML => export_animation_to_html(frames, path, config),
ExportFormat::JSON => export_animation_to_json(frames, path, config),
_ => Err(ClusteringError::ComputationError(format!(
"Unsupported export format {:?} for animation",
config.format
))),
}
}
#[allow(dead_code)]
#[allow(unused_variables)]
pub fn export_scatter_2d_to_json<P: AsRef<Path>>(
plot: &ScatterPlot2D,
output_path: P,
config: &ExportConfig,
) -> Result<()> {
#[cfg(feature = "serde")]
{
let export_data = Scatter2DExport {
format_version: "1.0".to_string(),
export_config: config.clone(),
plot_data: plot.clone(),
metadata: create_metadata(),
};
let json_string = serde_json::to_string_pretty(&export_data).map_err(|e| {
ClusteringError::ComputationError(format!("JSON serialization failed: {}", e))
})?;
std::fs::write(output_path, json_string)
.map_err(|e| ClusteringError::ComputationError(format!("File write failed: {}", e)))?;
return Ok(());
}
#[cfg(not(feature = "serde"))]
{
Err(ClusteringError::ComputationError(
"JSON export requires 'serde' feature".to_string(),
))
}
}
#[allow(dead_code)]
#[allow(unused_variables)]
pub fn export_scatter_3d_to_json<P: AsRef<Path>>(
plot: &ScatterPlot3D,
output_path: P,
config: &ExportConfig,
) -> Result<()> {
#[cfg(feature = "serde")]
{
let export_data = Scatter3DExport {
format_version: "1.0".to_string(),
export_config: config.clone(),
plot_data: plot.clone(),
metadata: create_metadata(),
};
let json_string = serde_json::to_string_pretty(&export_data).map_err(|e| {
ClusteringError::ComputationError(format!("JSON serialization failed: {}", e))
})?;
std::fs::write(output_path, json_string)
.map_err(|e| ClusteringError::ComputationError(format!("File write failed: {}", e)))?;
return Ok(());
}
#[cfg(not(feature = "serde"))]
{
Err(ClusteringError::ComputationError(
"JSON export requires 'serde' feature".to_string(),
))
}
}
#[allow(dead_code)]
pub fn export_scatter_2d_to_html<P: AsRef<Path>>(
plot: &ScatterPlot2D,
output_path: P,
config: &ExportConfig,
) -> Result<()> {
let html_content = generate_scatter_2d_html(plot, config)?;
std::fs::write(output_path, html_content)
.map_err(|e| ClusteringError::ComputationError(format!("File write failed: {}", e)))?;
Ok(())
}
#[allow(dead_code)]
pub fn export_scatter_3d_to_html<P: AsRef<Path>>(
plot: &ScatterPlot3D,
output_path: P,
config: &ExportConfig,
) -> Result<()> {
let html_content = generate_scatter_3d_html(plot, config)?;
std::fs::write(output_path, html_content)
.map_err(|e| ClusteringError::ComputationError(format!("File write failed: {}", e)))?;
Ok(())
}
#[allow(dead_code)]
pub fn save_visualization_to_file<P: AsRef<Path>>(
plot_2d: Option<&ScatterPlot2D>,
plot_3d: Option<&ScatterPlot3D>,
animation_frames: Option<&[AnimationFrame]>,
output_path: P,
mut config: ExportConfig,
) -> Result<()> {
let path = output_path.as_ref();
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
config.format = match extension.to_lowercase().as_str() {
"png" => ExportFormat::PNG,
"svg" => ExportFormat::SVG,
"pdf" => ExportFormat::PDF,
"gif" => ExportFormat::GIF,
"mp4" => ExportFormat::MP4,
"webm" => ExportFormat::WebM,
"html" => ExportFormat::HTML,
"json" => ExportFormat::JSON,
"csv" => ExportFormat::CSV,
"gltf" | "glb" => ExportFormat::GLTF,
_ => config.format, };
}
if let Some(_frames) = animation_frames {
export_animation_to_file(_frames, path, &config)
} else if let Some(plot_3d) = plot_3d {
export_scatter_3d_to_file(plot_3d, path, &config)
} else if let Some(plot_2d) = plot_2d {
export_scatter_2d_to_file(plot_2d, path, &config)
} else {
Err(ClusteringError::InvalidInput(
"No visualization data provided for export".to_string(),
))
}
}
#[allow(dead_code)]
fn generate_scatter_2d_html(plot: &ScatterPlot2D, config: &ExportConfig) -> Result<String> {
let plot_data = serde_json::json!({
"type": "scatter2d",
"data": "plot_data_placeholder" });
let plot_data_json = serde_json::to_string(&plot_data).map_err(|e| {
ClusteringError::ComputationError(format!("JSON serialization failed: {}", e))
})?;
const HTML_TEMPLATE: &str = "<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>Clustering Visualization</title>
<script src=\"https://d3js.org/d3.v7.min.js\"></script>
<style>
body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; background: {background}; }}
.container {{ max-width: 1200px; margin: 0 auto; }}
.visualization {{ border: 1px solid #ccc; border-radius: 8px; }}
.controls {{ margin: 20px 0; }}
.legend {{ margin-top: 20px; }}
.legend-item {{ display: inline-block; margin-right: 20px; }}
.legend-color {{ width: 20px; height: 20px; display: inline-block; margin-right: 5px; vertical-align: middle; }}
{custom_css}
</style>
</head>
<body>
<div class=\"container\">
<h1>Clustering Visualization</h1>
<div id=\"_plot\" class=\"visualization\"></div>
<div class=\"legend\" id=\"legend\"></div>
<div class=\"controls\">
<label>Point Size: <input type=\"range\" id=\"point-size\" min=\"1\" max=\"20\" value=\"{point_size}\"></label>
<label>Opacity: <input type=\"range\" id=\"opacity\" min=\"0\" max=\"100\" value=\"{opacity}\"></label>
</div>
</div>
<script>
const plotData = {plot_data};
const config = {{
width: {width},
height: {height},
interactive: {interactive}
}};
function createVisualization() {{
const svg = d3.select(\"HASH_PLOT\")
.append(\"svg\")
.attr(\"width\", config.width)
.attr(\"height\", config.height);
const margin = {{top: 20, right: 30, bottom: 40, left: 40}};
const width = config.width - margin.left - margin.right;
const height = config.height - margin.top - margin.bottom;
const g = svg.append(\"g\")
.attr(\"transform\", \"translate(\" + margin.left + \",\" + margin.top + \")\");
const xScale = d3.scaleLinear()
.domain(d3.extent(plotData.points.flat().filter((_, i) => i % 2 === 0)))
.range([0, width]);
const yScale = d3.scaleLinear()
.domain(d3.extent(plotData.points.flat().filter((_, i) => i % 2 === 1)))
.range([height, 0]);
g.append(\"g\")
.attr(\"transform\", \"translate(0,\" + height + \")\")
.call(d3.axisBottom(xScale));
g.append(\"g\")
.call(d3.axisLeft(yScale));
const points = [];
for (let i = 0; i < plotData.points.length; i++) {{
points.push({{
x: plotData.points[i][0],
y: plotData.points[i][1],
label: plotData.labels[i],
color: plotData.colors[i],
size: plotData.sizes[i]
}});
}}
g.selectAll(\"DOT_POINT\")
.data(points)
.enter().append(\"circle\")
.attr(\"class\", \"point\")
.attr(\"cx\", d => xScale(d.x))
.attr(\"cy\", d => yScale(d.y))
.attr(\"r\", d => d.size)
.attr(\"fill\", d => d.color)
.attr(\"opacity\", {opacity});
const legend = d3.select(\"HASH_LEGEND\");
plotData.legend.forEach(item => {{
const legendItem = legend.append(\"div\")
.attr(\"class\", \"legend-item\");
legendItem.append(\"div\")
.attr(\"class\", \"legend-color\")
.style(\"background-color\", item.color);
legendItem.append(\"span\")
.text(item.label + \" (\" + item.count + \" points)\");
}});
}}
createVisualization();
if (config.interactive) {{
d3.select(\"HASH_POINT_SIZE\").on(\"input\", function() {{
const size = +this.value;
d3.selectAll(\"DOT_POINT\").attr(\"r\", size);
}});
d3.select(\"HASH_OPACITY\").on(\"input\", function() {{
const opacity = +this.value / 100;
d3.selectAll(\"DOT_POINT\").attr(\"opacity\", opacity);
}});
}}
</script>
</body>
</html>";
let html_template = HTML_TEMPLATE
.replace("HASH_PLOT", "#_plot")
.replace("DOT_POINT", ".point")
.replace("HASH_LEGEND", "#legend")
.replace("HASH_POINT_SIZE", "#point-size")
.replace("HASH_OPACITY", "#opacity");
let html_content = html_template
.replace("{background}", &config.background_color)
.replace("{plot_data}", &plot_data_json)
.replace("{width}", &config.dimensions.0.to_string())
.replace("{height}", &config.dimensions.1.to_string())
.replace(
"{point_size}",
&plot.sizes.first().unwrap_or(&5.0).to_string(),
)
.replace("{opacity}", &(config.quality as f32 / 100.0).to_string())
.replace("{interactive}", &config.interactive.to_string())
.replace(
"{custom_css}",
config.custom_styling.as_deref().unwrap_or(""),
);
Ok(html_content)
}
#[allow(dead_code)]
fn generate_scatter_3d_html(plot: &ScatterPlot3D, config: &ExportConfig) -> Result<String> {
let plot_data = serde_json::json!({
"type": "scatter3d",
"data": "plot_data_placeholder" });
let plot_data_json = serde_json::to_string(&plot_data).map_err(|e| {
ClusteringError::ComputationError(format!("JSON serialization failed: {}", e))
})?;
let html_template = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Clustering Visualization</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<style>
body {{ font-family: Arial, sans-serif; margin: 0; padding: 0; overflow: hidden; background: {background}; }}
#container {{ width: 100vw; height: 100vh; }}
#info {{ position: absolute; top: 10px; left: 10px; color: white; z-index: 100; }}
.controls {{ position: absolute; top: 10px; right: 10px; z-index: 100; }}
{custom_css}
</style>
</head>
<body>
<div id="container"></div>
<div id="info">
<h2>3D Clustering Visualization</h2>
<p>Use mouse to rotate, scroll to zoom</p>
</div>
<script>
const plotData = {plot_data};
let scene, camera, renderer, controls;
let pointsGroup;
function init() {{
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color('{background}');
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(10, 10, 10);
// Renderer
renderer = new THREE.WebGLRenderer({{ antialias: true }});
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// Controls (basic orbit controls implementation)
setupControls();
// Add coordinate axes
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// Add grid
const gridHelper = new THREE.GridHelper(20, 20);
scene.add(gridHelper);
// Create points
createPoints();
// Add lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
directionalLight.position.set(10, 10, 5);
scene.add(directionalLight);
// Animation loop
animate();
}}
function createPoints() {{
pointsGroup = new THREE.Group();
const geometry = new THREE.SphereGeometry(0.1, 8, 6);
for (let i = 0; i < plotData.points.length; i++) {{
const material = new THREE.MeshLambertMaterial({{
color: plotData.colors[i],
transparent: true,
opacity: {opacity}
}});
const point = new THREE.Mesh(geometry, material);
point.position.set(
plotData.points[i][0],
plotData.points[i][1],
plotData.points[i][2]
);
point.scale.setScalar(plotData.sizes[i] * 0.1);
pointsGroup.add(point);
}}
scene.add(pointsGroup);
// Add centroids if available
if (plotData.centroids) {{
const centroidGeometry = new THREE.SphereGeometry(0.2, 16, 12);
for (let i = 0; i < plotData.centroids.length; i++) {{
const material = new THREE.MeshLambertMaterial({{
color: 0xff0000,
transparent: true,
opacity: 0.8
}});
const centroid = new THREE.Mesh(centroidGeometry, material);
centroid.position.set(
plotData.centroids[i][0],
plotData.centroids[i][1],
plotData.centroids[i][2]
);
scene.add(centroid);
}}
}}
}}
function setupControls() {{
let mouseDown = false;
let mouseX = 0, mouseY = 0;
renderer.domElement.addEventListener('mousedown', (event) => {{
mouseDown = true;
mouseX = event.clientX;
mouseY = event.clientY;
}});
renderer.domElement.addEventListener('mouseup', () => {{
mouseDown = false;
}});
renderer.domElement.addEventListener('mousemove', (event) => {{
if (!mouseDown) return;
const deltaX = event.clientX - mouseX;
const deltaY = event.clientY - mouseY;
// Rotate camera around the scene
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
spherical.theta -= deltaX * 0.01;
spherical.phi += deltaY * 0.01;
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
mouseX = event.clientX;
mouseY = event.clientY;
}});
renderer.domElement.addEventListener('wheel', (event) => {{
const scale = event.deltaY > 0 ? 1.1 : 0.9;
camera.position.multiplyScalar(scale);
}});
}}
function animate() {{
requestAnimationFrame(animate);
renderer.render(scene, camera);
}}
function onWindowResize() {{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}}
window.addEventListener('resize', onWindowResize);
init();
</script>
</body>
</html>"#;
let html_content = html_template
.replace("{background}", &config.background_color)
.replace("{plot_data}", &plot_data_json)
.replace("{opacity}", &(config.quality as f32 / 100.0).to_string())
.replace(
"{custom_css}",
config.custom_styling.as_deref().unwrap_or(""),
);
Ok(html_content)
}
#[allow(dead_code)]
fn export_scatter_2d_to_csv<P: AsRef<Path>>(
plot: &ScatterPlot2D,
output_path: P,
_config: &ExportConfig,
) -> Result<()> {
let mut csv_content = String::from("x,y,cluster,color\n");
for i in 0..plot.points.nrows() {
csv_content.push_str(&format!(
"{},{},{},{}\n",
plot.points[[i, 0]],
plot.points[[i, 1]],
plot.labels[i],
plot.colors[i]
));
}
std::fs::write(output_path, csv_content)
.map_err(|e| ClusteringError::ComputationError(format!("File write failed: {}", e)))?;
Ok(())
}
#[allow(dead_code)]
fn export_scatter_2d_to_plotly<P: AsRef<Path>>(
_plot: &ScatterPlot2D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"Plotly export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_2d_to_d3<P: AsRef<Path>>(
_plot: &ScatterPlot2D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"D3.js export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_2d_to_svg<P: AsRef<Path>>(
_plot: &ScatterPlot2D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"SVG export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_2d_to_png<P: AsRef<Path>>(
_plot: &ScatterPlot2D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"PNG export requires image rendering library".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_3d_to_threejs<P: AsRef<Path>>(
_plot: &ScatterPlot3D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"Three.js export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_3d_to_gltf<P: AsRef<Path>>(
_plot: &ScatterPlot3D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"GLTF export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_3d_to_webgl<P: AsRef<Path>>(
_plot: &ScatterPlot3D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"WebGL export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_3d_to_unity<P: AsRef<Path>>(
_plot: &ScatterPlot3D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"Unity3D export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_scatter_3d_to_blender<P: AsRef<Path>>(
_plot: &ScatterPlot3D,
path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"Blender export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
fn export_animation_to_gif<P: AsRef<Path>>(
_frames: &[AnimationFrame],
_output_path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"GIF export requires animation library".to_string(),
))
}
#[allow(dead_code)]
fn export_animation_to_mp4<P: AsRef<Path>>(
_frames: &[AnimationFrame],
_output_path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"MP4 export requires video encoding library".to_string(),
))
}
#[allow(dead_code)]
fn export_animation_to_webm<P: AsRef<Path>>(
_frames: &[AnimationFrame],
_output_path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"WebM export requires video encoding library".to_string(),
))
}
#[allow(dead_code)]
fn export_animation_to_html<P: AsRef<Path>>(
_frames: &[AnimationFrame],
_output_path: P,
_config: &ExportConfig,
) -> Result<()> {
Err(ClusteringError::ComputationError(
"Animation HTML export not yet implemented".to_string(),
))
}
#[allow(dead_code)]
#[allow(unused_variables)]
fn export_animation_to_json<P: AsRef<Path>>(
frames: &[AnimationFrame],
output_path: P,
_config: &ExportConfig,
) -> Result<()> {
#[cfg(feature = "serde")]
{
let json_string = serde_json::to_string_pretty(frames).map_err(|e| {
ClusteringError::ComputationError(format!("JSON serialization failed: {}", e))
})?;
std::fs::write(output_path, json_string)
.map_err(|e| ClusteringError::ComputationError(format!("File write failed: {}", e)))?;
return Ok(());
}
#[cfg(not(feature = "serde"))]
{
Err(ClusteringError::ComputationError(
"JSON export requires 'serde' feature".to_string(),
))
}
}
#[allow(dead_code)]
fn create_metadata() -> ExportMetadata {
ExportMetadata {
created_at: chrono::Utc::now().to_rfc3339(),
software: "scirs2-cluster".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
format_version: "1.0".to_string(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ExportMetadata {
created_at: String,
software: String,
version: String,
format_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Scatter2DExport {
format_version: String,
export_config: ExportConfig,
plot_data: ScatterPlot2D,
metadata: ExportMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Scatter3DExport {
format_version: String,
export_config: ExportConfig,
plot_data: ScatterPlot3D,
metadata: ExportMetadata,
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_export_config_defaults() {
let config = ExportConfig::default();
assert_eq!(config.format, ExportFormat::PNG);
assert_eq!(config.dimensions, (1920, 1080));
assert_eq!(config.dpi, 300);
}
#[test]
fn test_scatter_2d_csv_export() {
let plot = ScatterPlot2D {
points: Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
.expect("Operation failed"),
labels: Array1::from_vec(vec![0, 1]),
centroids: None,
colors: vec!["#FF0000".to_string(), "#00FF00".to_string()],
sizes: vec![5.0, 5.0],
point_labels: None,
bounds: (0.0, 4.0, 0.0, 4.0),
legend: Vec::new(),
};
let config = ExportConfig {
format: ExportFormat::CSV,
..Default::default()
};
let temp_file = tempfile::NamedTempFile::new().expect("Operation failed");
export_scatter_2d_to_csv(&plot, temp_file.path(), &config).expect("Operation failed");
let content = std::fs::read_to_string(temp_file.path()).expect("Operation failed");
assert!(content.contains("x,y,cluster,color"));
assert!(content.contains("1,2,0,#FF0000"));
}
}
use chrono;