oxirs_samm/graph_analytics/
visualization.rs1use crate::error::{Result, SammError};
36use crate::graph_analytics::ModelGraph;
37use std::fmt::Write as FmtWrite;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum VisualizationStyle {
42 Compact,
44 #[default]
46 Detailed,
47 Hierarchical,
49}
50
51#[derive(Debug, Clone)]
53pub struct ColorScheme {
54 pub aspect_color: String,
56 pub property_color: String,
58 pub characteristic_color: String,
60 pub edge_color: String,
62}
63
64impl Default for ColorScheme {
65 fn default() -> Self {
66 Self {
67 aspect_color: "#E8F4F8".to_string(), property_color: "#FFF4E6".to_string(), characteristic_color: "#F0F0F0".to_string(), edge_color: "#666666".to_string(), }
72 }
73}
74
75impl ModelGraph {
76 pub fn to_dot(&self, style: VisualizationStyle) -> Result<String> {
101 self.to_dot_with_colors(style, ColorScheme::default())
102 }
103
104 pub fn to_dot_with_colors(
115 &self,
116 style: VisualizationStyle,
117 colors: ColorScheme,
118 ) -> Result<String> {
119 let mut dot = String::new();
120
121 writeln!(dot, "digraph SAMM_Model {{")
123 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
124 writeln!(dot, " // Generated by OxiRS SAMM")
125 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
126 writeln!(
127 dot,
128 " rankdir={}; // Layout direction",
129 if style == VisualizationStyle::Hierarchical {
130 "TB"
131 } else {
132 "LR"
133 }
134 )
135 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
136 writeln!(dot, " node [shape=box, style=filled, fontname=\"Arial\"];")
137 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
138 writeln!(dot, " edge [color=\"{}\"];", colors.edge_color)
139 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
140 writeln!(dot).map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
141
142 for name in self.nodes() {
144 let (color, shape, label) = self.get_node_attributes(name, style, &colors);
145 writeln!(
146 dot,
147 " \"{}\" [fillcolor=\"{}\", shape={}, label=\"{}\"];",
148 name, color, shape, label
149 )
150 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
151 }
152
153 writeln!(dot).map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
154
155 for (src, tgt) in self.edges() {
157 writeln!(dot, " \"{}\" -> \"{}\";", src, tgt)
158 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
159 }
160
161 writeln!(dot, "}}")
162 .map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
163
164 Ok(dot)
165 }
166
167 fn get_node_attributes(
169 &self,
170 name: &str,
171 style: VisualizationStyle,
172 colors: &ColorScheme,
173 ) -> (String, &'static str, String) {
174 let (color, shape) = if name.contains("Aspect") {
176 (colors.aspect_color.clone(), "box")
177 } else if name.contains("Characteristic") || name.contains("Char") {
178 (colors.characteristic_color.clone(), "ellipse")
179 } else {
180 (colors.property_color.clone(), "rectangle")
182 };
183
184 let label = match style {
186 VisualizationStyle::Compact => {
187 name.to_string()
189 }
190 VisualizationStyle::Detailed => {
191 name.to_string()
193 }
194 VisualizationStyle::Hierarchical => {
195 name.to_string()
197 }
198 };
199
200 (color, shape, label)
201 }
202
203 #[cfg(feature = "graphviz")]
226 pub fn render_svg(&self, output_path: &str, style: VisualizationStyle) -> Result<()> {
227 use graphviz_rust::cmd::{CommandArg, Format};
228 use graphviz_rust::exec;
229 use graphviz_rust::parse;
230 use graphviz_rust::printer::PrinterContext;
231
232 let dot_string = self.to_dot(style)?;
233
234 let graph = parse(&dot_string)
236 .map_err(|e| SammError::GraphError(format!("Failed to parse DOT: {:?}", e)))?;
237
238 let svg = exec(
240 graph,
241 &mut PrinterContext::default(),
242 vec![CommandArg::Format(Format::Svg)],
243 )
244 .map_err(|e| SammError::GraphError(format!("Failed to render SVG: {:?}", e)))?;
245
246 std::fs::write(output_path, svg)
248 .map_err(|e| SammError::GraphError(format!("Failed to write SVG: {}", e)))?;
249
250 Ok(())
251 }
252
253 #[cfg(feature = "graphviz")]
276 pub fn render_png(&self, output_path: &str, style: VisualizationStyle) -> Result<()> {
277 use graphviz_rust::cmd::{CommandArg, Format};
278 use graphviz_rust::exec;
279 use graphviz_rust::parse;
280 use graphviz_rust::printer::PrinterContext;
281
282 let dot_string = self.to_dot(style)?;
283
284 let graph = parse(&dot_string)
286 .map_err(|e| SammError::GraphError(format!("Failed to parse DOT: {:?}", e)))?;
287
288 let png = exec(
290 graph,
291 &mut PrinterContext::default(),
292 vec![CommandArg::Format(Format::Png)],
293 )
294 .map_err(|e| SammError::GraphError(format!("Failed to render PNG: {:?}", e)))?;
295
296 std::fs::write(output_path, png)
298 .map_err(|e| SammError::GraphError(format!("Failed to write PNG: {}", e)))?;
299
300 Ok(())
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::metamodel::{Aspect, Characteristic, CharacteristicKind, Property};
308
309 fn create_test_aspect() -> Aspect {
310 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
311
312 for i in 1..=3 {
314 let characteristic = Characteristic {
315 metadata: crate::metamodel::ElementMetadata::new(format!(
316 "urn:samm:test:1.0.0#Char{}",
317 i
318 )),
319 data_type: Some("string".to_string()),
320 kind: CharacteristicKind::Trait,
321 constraints: vec![],
322 };
323
324 let property = Property::new(format!("urn:samm:test:1.0.0#Property{}", i))
325 .with_characteristic(characteristic);
326
327 aspect.add_property(property);
328 }
329
330 aspect
331 }
332
333 #[test]
334 fn test_dot_generation_compact() {
335 let aspect = create_test_aspect();
336 let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
337
338 let dot = graph
339 .to_dot(VisualizationStyle::Compact)
340 .expect("operation should succeed");
341
342 assert!(dot.contains("digraph SAMM_Model"));
344 assert!(dot.contains("TestAspect"));
345 assert!(dot.contains("Property1"));
346 assert!(dot.contains("Char1"));
347 }
348
349 #[test]
350 fn test_dot_generation_detailed() {
351 let aspect = create_test_aspect();
352 let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
353
354 let dot = graph
355 .to_dot(VisualizationStyle::Detailed)
356 .expect("operation should succeed");
357
358 assert!(dot.contains("digraph SAMM_Model"));
360 assert!(dot.contains("fillcolor"));
361 assert!(dot.contains("->"));
362 }
363
364 #[test]
365 fn test_dot_generation_hierarchical() {
366 let aspect = create_test_aspect();
367 let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
368
369 let dot = graph
370 .to_dot(VisualizationStyle::Hierarchical)
371 .expect("operation should succeed");
372
373 assert!(dot.contains("rankdir=TB"));
375 }
376
377 #[test]
378 fn test_custom_colors() {
379 let aspect = create_test_aspect();
380 let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
381
382 let colors = ColorScheme {
383 aspect_color: "#FF0000".to_string(),
384 property_color: "#00FF00".to_string(),
385 characteristic_color: "#0000FF".to_string(),
386 edge_color: "#000000".to_string(),
387 };
388
389 let dot = graph
390 .to_dot_with_colors(VisualizationStyle::Detailed, colors)
391 .expect("operation should succeed");
392
393 assert!(dot.contains("#FF0000") || dot.contains("#00FF00") || dot.contains("#0000FF"));
395 }
396
397 #[test]
398 fn test_node_attributes() {
399 let aspect = create_test_aspect();
400 let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
401 let colors = ColorScheme::default();
402
403 let (color, shape, _label) =
405 graph.get_node_attributes("TestAspect", VisualizationStyle::Detailed, &colors);
406 assert_eq!(color, colors.aspect_color);
407 assert_eq!(shape, "box");
408
409 let (color, shape, _label) =
411 graph.get_node_attributes("Char1", VisualizationStyle::Detailed, &colors);
412 assert_eq!(color, colors.characteristic_color);
413 assert_eq!(shape, "ellipse");
414
415 let (color, shape, _label) =
417 graph.get_node_attributes("Property1", VisualizationStyle::Detailed, &colors);
418 assert_eq!(color, colors.property_color);
419 assert_eq!(shape, "rectangle");
420 }
421}