use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VisualizationType {
Table,
BarChart,
LineChart,
PieChart,
Network,
Tree,
Timeline,
Heatmap,
GeoMap,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisualizationConfig {
pub viz_type: VisualizationType,
pub title: String,
pub width: String,
pub height: String,
pub color_scheme: ColorScheme,
pub interactive: bool,
pub options: HashMap<String, serde_json::Value>,
}
impl Default for VisualizationConfig {
fn default() -> Self {
Self {
viz_type: VisualizationType::Table,
title: "Data Visualization".to_string(),
width: "100%".to_string(),
height: "400px".to_string(),
color_scheme: ColorScheme::Default,
interactive: true,
options: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ColorScheme {
Default,
Blue,
Green,
Red,
Purple,
Grayscale,
Rainbow,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPoint {
pub x: String,
pub y: f64,
pub label: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkNode {
pub id: String,
pub label: String,
pub node_type: String,
pub size: f32,
pub color: Option<String>,
pub position: Option<(f64, f64)>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkEdge {
pub source: String,
pub target: String,
pub label: Option<String>,
pub weight: f32,
pub color: Option<String>,
pub directed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisualizationSpec {
pub config: VisualizationConfig,
pub data: VisualizationData,
pub rendering_hints: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum VisualizationData {
Table {
columns: Vec<String>,
rows: Vec<Vec<String>>,
},
Chart { series: Vec<ChartSeries> },
Network {
nodes: Vec<NetworkNode>,
edges: Vec<NetworkEdge>,
},
Tree { root: TreeNode },
Timeline { events: Vec<TimelineEvent> },
Heatmap {
data: Vec<Vec<f64>>,
x_labels: Vec<String>,
y_labels: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChartSeries {
pub name: String,
pub data: Vec<DataPoint>,
pub color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeNode {
pub id: String,
pub label: String,
pub children: Vec<TreeNode>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineEvent {
pub id: String,
pub title: String,
pub description: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub duration: Option<chrono::Duration>,
pub category: Option<String>,
}
pub struct VisualizationBuilder {
spec: VisualizationSpec,
}
impl VisualizationBuilder {
pub fn new(viz_type: VisualizationType) -> Self {
Self {
spec: VisualizationSpec {
config: VisualizationConfig {
viz_type,
..Default::default()
},
data: VisualizationData::Table {
columns: vec![],
rows: vec![],
},
rendering_hints: HashMap::new(),
},
}
}
pub fn title(mut self, title: String) -> Self {
self.spec.config.title = title;
self
}
pub fn dimensions(mut self, width: String, height: String) -> Self {
self.spec.config.width = width;
self.spec.config.height = height;
self
}
pub fn color_scheme(mut self, scheme: ColorScheme) -> Self {
self.spec.config.color_scheme = scheme;
self
}
pub fn interactive(mut self, interactive: bool) -> Self {
self.spec.config.interactive = interactive;
self
}
pub fn option<K: Into<String>, V: Serialize>(mut self, key: K, value: V) -> Self {
self.spec.config.options.insert(
key.into(),
serde_json::to_value(value).expect("serializable value should convert to JSON"),
);
self
}
pub fn table_data(mut self, columns: Vec<String>, rows: Vec<Vec<String>>) -> Self {
self.spec.data = VisualizationData::Table { columns, rows };
self
}
pub fn chart_data(mut self, series: Vec<ChartSeries>) -> Self {
self.spec.data = VisualizationData::Chart { series };
self
}
pub fn network_data(mut self, nodes: Vec<NetworkNode>, edges: Vec<NetworkEdge>) -> Self {
self.spec.data = VisualizationData::Network { nodes, edges };
self
}
pub fn tree_data(mut self, root: TreeNode) -> Self {
self.spec.data = VisualizationData::Tree { root };
self
}
pub fn timeline_data(mut self, events: Vec<TimelineEvent>) -> Self {
self.spec.data = VisualizationData::Timeline { events };
self
}
pub fn heatmap_data(
mut self,
data: Vec<Vec<f64>>,
x_labels: Vec<String>,
y_labels: Vec<String>,
) -> Self {
self.spec.data = VisualizationData::Heatmap {
data,
x_labels,
y_labels,
};
self
}
pub fn hint<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.spec.rendering_hints.insert(key.into(), value.into());
self
}
pub fn build(self) -> VisualizationSpec {
self.spec
}
}
pub struct VisualizationRenderer;
impl VisualizationRenderer {
pub fn to_json(spec: &VisualizationSpec) -> Result<String> {
Ok(serde_json::to_string_pretty(spec)?)
}
pub fn to_vega_lite(spec: &VisualizationSpec) -> Result<String> {
debug!("Converting to Vega-Lite specification");
let vega_spec = match &spec.data {
VisualizationData::Chart { series } => {
let mut data_values = Vec::new();
for s in series {
for point in &s.data {
data_values.push(serde_json::json!({
"x": point.x,
"y": point.y,
"series": s.name,
}));
}
}
serde_json::json!({
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": spec.config.title,
"width": 600,
"height": 400,
"data": {
"values": data_values
},
"mark": "line",
"encoding": {
"x": {"field": "x", "type": "ordinal"},
"y": {"field": "y", "type": "quantitative"},
"color": {"field": "series", "type": "nominal"}
}
})
}
_ => {
serde_json::json!({
"error": "Visualization type not yet supported for Vega-Lite"
})
}
};
Ok(serde_json::to_string_pretty(&vega_spec)?)
}
pub fn to_plantuml(spec: &VisualizationSpec) -> Result<String> {
debug!("Converting to PlantUML");
match &spec.data {
VisualizationData::Network { nodes, edges } => {
let mut plantuml = String::from("@startuml\n");
for node in nodes {
plantuml.push_str(&format!("object {} {{\n", node.id));
plantuml.push_str(&format!(" label = \"{}\"\n", node.label));
plantuml.push_str("}\n");
}
for edge in edges {
let arrow = if edge.directed { "-->" } else { "--" };
if let Some(label) = &edge.label {
plantuml.push_str(&format!(
"{} {} {} : {}\n",
edge.source, arrow, edge.target, label
));
} else {
plantuml.push_str(&format!("{} {} {}\n", edge.source, arrow, edge.target));
}
}
plantuml.push_str("@enduml\n");
Ok(plantuml)
}
_ => Err(anyhow::anyhow!(
"PlantUML rendering only supported for network visualizations"
)),
}
}
pub fn to_html_table(spec: &VisualizationSpec) -> Result<String> {
match &spec.data {
VisualizationData::Table { columns, rows } => {
let mut html = String::from("<table>\n");
html.push_str(" <thead>\n <tr>\n");
for col in columns {
html.push_str(&format!(" <th>{}</th>\n", col));
}
html.push_str(" </tr>\n </thead>\n");
html.push_str(" <tbody>\n");
for row in rows {
html.push_str(" <tr>\n");
for cell in row {
html.push_str(&format!(" <td>{}</td>\n", cell));
}
html.push_str(" </tr>\n");
}
html.push_str(" </tbody>\n");
html.push_str("</table>");
Ok(html)
}
_ => Err(anyhow::anyhow!(
"HTML table rendering only supported for table visualizations"
)),
}
}
pub fn to_mermaid(spec: &VisualizationSpec) -> Result<String> {
match &spec.data {
VisualizationData::Network { nodes, edges } => {
let mut mermaid = String::from("graph TD\n");
for node in nodes {
mermaid.push_str(&format!(" {}[\"{}\"]\n", node.id, node.label));
}
for edge in edges {
if let Some(label) = &edge.label {
mermaid.push_str(&format!(
" {} -->|{}| {}\n",
edge.source, label, edge.target
));
} else {
mermaid.push_str(&format!(" {} --> {}\n", edge.source, edge.target));
}
}
Ok(mermaid)
}
VisualizationData::Tree { root } => {
let mut mermaid = String::from("graph TD\n");
fn add_tree_node(mermaid: &mut String, node: &TreeNode, parent_id: Option<&str>) {
mermaid.push_str(&format!(" {}[\"{}\"]\n", node.id, node.label));
if let Some(parent) = parent_id {
mermaid.push_str(&format!(" {} --> {}\n", parent, node.id));
}
for child in &node.children {
add_tree_node(mermaid, child, Some(&node.id));
}
}
add_tree_node(&mut mermaid, root, None);
Ok(mermaid)
}
_ => Err(anyhow::anyhow!(
"Mermaid rendering supported for network and tree visualizations"
)),
}
}
}
pub mod helpers {
use super::*;
pub fn bar_chart(title: String, data: Vec<(String, f64)>) -> VisualizationSpec {
let series = ChartSeries {
name: "Values".to_string(),
data: data
.into_iter()
.map(|(x, y)| DataPoint {
x,
y,
label: None,
metadata: HashMap::new(),
})
.collect(),
color: None,
};
VisualizationBuilder::new(VisualizationType::BarChart)
.title(title)
.chart_data(vec![series])
.build()
}
pub fn network_graph(
title: String,
nodes: Vec<(String, String)>,
edges: Vec<(String, String, Option<String>)>,
) -> VisualizationSpec {
let network_nodes: Vec<NetworkNode> = nodes
.into_iter()
.map(|(id, label)| NetworkNode {
id,
label,
node_type: "default".to_string(),
size: 1.0,
color: None,
position: None,
metadata: HashMap::new(),
})
.collect();
let network_edges: Vec<NetworkEdge> = edges
.into_iter()
.map(|(source, target, label)| NetworkEdge {
source,
target,
label,
weight: 1.0,
color: None,
directed: true,
})
.collect();
VisualizationBuilder::new(VisualizationType::Network)
.title(title)
.network_data(network_nodes, network_edges)
.build()
}
pub fn timeline(title: String, events: Vec<TimelineEvent>) -> VisualizationSpec {
VisualizationBuilder::new(VisualizationType::Timeline)
.title(title)
.timeline_data(events)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_visualization_builder() {
let spec = VisualizationBuilder::new(VisualizationType::Table)
.title("Test Table".to_string())
.table_data(
vec!["Col1".to_string(), "Col2".to_string()],
vec![
vec!["A".to_string(), "B".to_string()],
vec!["C".to_string(), "D".to_string()],
],
)
.build();
assert_eq!(spec.config.title, "Test Table");
match spec.data {
VisualizationData::Table { columns, rows } => {
assert_eq!(columns.len(), 2);
assert_eq!(rows.len(), 2);
}
_ => panic!("Expected table data"),
}
}
#[test]
fn test_bar_chart_helper() {
let spec = helpers::bar_chart(
"Sales".to_string(),
vec![
("Q1".to_string(), 100.0),
("Q2".to_string(), 150.0),
("Q3".to_string(), 120.0),
],
);
assert_eq!(spec.config.viz_type, VisualizationType::BarChart);
assert_eq!(spec.config.title, "Sales");
}
#[test]
fn test_network_graph_helper() {
let spec = helpers::network_graph(
"Knowledge Graph".to_string(),
vec![("A".to_string(), "Node A".to_string())],
vec![(
"A".to_string(),
"B".to_string(),
Some("relates".to_string()),
)],
);
assert_eq!(spec.config.viz_type, VisualizationType::Network);
}
#[test]
fn test_html_table_rendering() {
let spec = VisualizationBuilder::new(VisualizationType::Table)
.table_data(
vec!["Name".to_string(), "Value".to_string()],
vec![vec!["Test".to_string(), "123".to_string()]],
)
.build();
let html = VisualizationRenderer::to_html_table(&spec).expect("should succeed");
assert!(html.contains("<table>"));
assert!(html.contains("<th>Name</th>"));
assert!(html.contains("<td>Test</td>"));
}
}