use crate::graph::Graph;
use crate::spacing::{SpacingConfig, SpacingMode};
use crate::style::display_width;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ScalingMode {
Auto,
#[default]
Fixed,
}
impl std::str::FromStr for ScalingMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"auto" | "automatic" | "adaptive" => Ok(ScalingMode::Auto),
"fixed" | "static" | "manual" => Ok(ScalingMode::Fixed),
_ => Err(()),
}
}
}
#[derive(Debug, Clone)]
pub struct DiagramMetrics {
pub node_count: usize,
pub edge_count: usize,
pub max_label_width: usize,
pub max_depth: usize,
pub max_layer_width: usize,
pub complexity_score: f32,
}
impl DiagramMetrics {
pub fn from_graph(graph: &Graph) -> Self {
let node_count = graph.nodes.len();
let edge_count = graph.edges.iter().filter(|e| !e.is_back_edge).count();
let max_label_width = graph
.nodes
.iter()
.map(|n| display_width(n.label.as_str()))
.max()
.unwrap_or(0);
let max_depth = if graph.nodes.is_empty() {
0
} else {
graph.nodes.iter().map(|n| n.rank).max().unwrap_or(0) + 1
};
let mut rank_counts: std::collections::HashMap<usize, usize> =
std::collections::HashMap::new();
for node in &graph.nodes {
*rank_counts.entry(node.rank).or_insert(0) += 1;
}
let max_layer_width = rank_counts.values().max().copied().unwrap_or(0);
let complexity_score =
Self::compute_complexity(node_count, edge_count, max_depth, max_layer_width);
Self {
node_count,
edge_count,
max_label_width,
max_depth,
max_layer_width,
complexity_score,
}
}
fn compute_complexity(
node_count: usize,
edge_count: usize,
max_depth: usize,
max_layer_width: usize,
) -> f32 {
let node_factor = node_count as f32 * 0.4;
let edge_factor = edge_count as f32 * 0.3;
let depth_factor = max_depth as f32 * 0.15;
let width_factor = max_layer_width as f32 * 0.15;
(node_factor + edge_factor + depth_factor + width_factor) / 10.0
}
pub fn recommended_spacing_mode(&self) -> SpacingMode {
if self.complexity_score < 2.0 {
SpacingMode::Spacious
} else if self.complexity_score <= 4.0 {
SpacingMode::Default
} else {
SpacingMode::Compact
}
}
pub fn is_dense(&self) -> bool {
if self.max_depth == 0 {
return false;
}
let avg_nodes_per_layer = self.node_count as f32 / self.max_depth as f32;
avg_nodes_per_layer > 4.0
}
}
#[derive(Debug, Clone)]
pub struct CanvasBudget {
pub max_width: usize,
pub target_width: Option<usize>,
pub max_height: usize,
pub target_height: Option<usize>,
}
impl Default for CanvasBudget {
fn default() -> Self {
Self {
max_width: 500,
target_width: None,
max_height: 200,
target_height: None,
}
}
}
impl CanvasBudget {
pub fn from_terminal() -> Self {
let mut budget = Self::default();
if let Ok(term_width) = std::env::var("COLUMNS") {
if let Ok(w) = term_width.parse::<usize>() {
budget.target_width = Some(w.saturating_sub(2)); }
}
if let Ok(term_height) = std::env::var("LINES") {
if let Ok(h) = term_height.parse::<usize>() {
budget.target_height = Some(h.saturating_sub(2)); }
}
budget
}
pub fn with_dimensions(width: usize, height: usize) -> Self {
Self {
max_width: width,
target_width: Some(width),
max_height: height,
target_height: Some(height),
}
}
pub fn compute_spacing(&self, metrics: &DiagramMetrics) -> SpacingConfig {
let base_mode = metrics.recommended_spacing_mode();
let mut config = SpacingConfig::from_mode(base_mode);
if let Some(target_width) = self.target_width {
if metrics.max_layer_width > 0 && metrics.max_label_width > 0 {
let estimated_width = estimate_canvas_width(metrics, &config);
if estimated_width > target_width {
let compact = SpacingConfig::compact();
let compact_width = estimate_canvas_width(metrics, &compact);
if compact_width <= target_width {
config = compact;
} else {
config = compact;
let excess = compact_width - target_width;
let label_reduction = excess / metrics.max_layer_width.max(1);
config.max_label_width = config
.max_label_width
.saturating_sub(label_reduction)
.max(8); }
} else if estimated_width < target_width / 2 && metrics.complexity_score < 2.0 {
config = SpacingConfig::spacious();
}
}
}
config
}
pub fn effective_width(&self) -> usize {
self.target_width
.unwrap_or(self.max_width)
.min(self.max_width)
}
pub fn effective_height(&self) -> usize {
self.target_height
.unwrap_or(self.max_height)
.min(self.max_height)
}
}
fn estimate_canvas_width(metrics: &DiagramMetrics, spacing: &SpacingConfig) -> usize {
if metrics.max_layer_width == 0 {
return 0;
}
let avg_label_width = metrics.max_label_width.min(spacing.max_label_width);
let box_width = avg_label_width + spacing.box_padding * 2 + 2;
let node_width = box_width.max(spacing.box_min_width);
metrics.max_layer_width * node_width
+ (metrics.max_layer_width.saturating_sub(1)) * spacing.col_spacing
+ spacing.col_spacing * 2 }
#[allow(dead_code)]
fn estimate_canvas_height(metrics: &DiagramMetrics, spacing: &SpacingConfig) -> usize {
if metrics.max_depth == 0 {
return 0;
}
metrics.max_depth * spacing.box_height
+ (metrics.max_depth.saturating_sub(1)) * spacing.row_spacing
+ spacing.row_spacing * 2 }
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{Edge, Node};
fn make_test_graph(node_count: usize, depth: usize) -> Graph {
let mut graph = Graph::new();
let nodes_per_layer = node_count / depth.max(1);
for i in 0..node_count {
let label = format!("Node{}", i);
let mut node = Node::new(&format!("n{}", i), &label);
node.rank = i / nodes_per_layer.max(1);
graph.nodes.push(node);
}
for i in 0..node_count.saturating_sub(1) {
if i + nodes_per_layer.max(1) < node_count {
graph.edges.push(Edge::new(
&format!("n{}", i),
&format!("n{}", i + nodes_per_layer.max(1)),
));
}
}
graph
}
#[test]
fn test_metrics_from_empty_graph() {
let graph = Graph::new();
let metrics = DiagramMetrics::from_graph(&graph);
assert_eq!(metrics.node_count, 0);
assert_eq!(metrics.edge_count, 0);
assert_eq!(metrics.max_depth, 0);
assert_eq!(metrics.complexity_score, 0.0);
}
#[test]
fn test_metrics_from_simple_graph() {
let graph = make_test_graph(4, 2);
let metrics = DiagramMetrics::from_graph(&graph);
assert_eq!(metrics.node_count, 4);
assert!(metrics.edge_count > 0);
assert!(metrics.max_depth >= 2);
}
#[test]
fn test_complexity_scaling() {
let small = make_test_graph(5, 2);
let medium = make_test_graph(20, 4);
let large = make_test_graph(50, 5);
let small_metrics = DiagramMetrics::from_graph(&small);
let medium_metrics = DiagramMetrics::from_graph(&medium);
let large_metrics = DiagramMetrics::from_graph(&large);
assert!(small_metrics.complexity_score < medium_metrics.complexity_score);
assert!(medium_metrics.complexity_score < large_metrics.complexity_score);
}
#[test]
fn test_recommended_spacing() {
let small = make_test_graph(3, 2);
let large = make_test_graph(100, 10);
let small_metrics = DiagramMetrics::from_graph(&small);
let large_metrics = DiagramMetrics::from_graph(&large);
let small_mode = small_metrics.recommended_spacing_mode();
assert!(matches!(
small_mode,
SpacingMode::Spacious | SpacingMode::Default
));
assert_eq!(
large_metrics.recommended_spacing_mode(),
SpacingMode::Compact
);
}
#[test]
fn test_canvas_budget_default() {
let budget = CanvasBudget::default();
assert_eq!(budget.max_width, 500);
assert_eq!(budget.max_height, 200);
assert!(budget.target_width.is_none());
}
#[test]
fn test_canvas_budget_with_dimensions() {
let budget = CanvasBudget::with_dimensions(80, 24);
assert_eq!(budget.effective_width(), 80);
assert_eq!(budget.effective_height(), 24);
}
#[test]
fn test_compute_spacing_for_wide_diagram() {
let mut graph = Graph::new();
for i in 0..10 {
let mut node = Node::new(&format!("n{}", i), "LongLabel123");
node.rank = 0;
graph.nodes.push(node);
}
let metrics = DiagramMetrics::from_graph(&graph);
let budget = CanvasBudget::with_dimensions(80, 24);
let spacing = budget.compute_spacing(&metrics);
assert!(spacing.col_spacing <= SpacingConfig::default().col_spacing);
}
#[test]
fn test_scaling_mode_parse() {
assert_eq!("auto".parse(), Ok(ScalingMode::Auto));
assert_eq!("automatic".parse(), Ok(ScalingMode::Auto));
assert_eq!("fixed".parse(), Ok(ScalingMode::Fixed));
assert_eq!("static".parse(), Ok(ScalingMode::Fixed));
assert!("invalid".parse::<ScalingMode>().is_err());
}
#[test]
fn test_width_estimation() {
let graph = make_test_graph(6, 3);
let metrics = DiagramMetrics::from_graph(&graph);
let spacing = SpacingConfig::default();
let width = estimate_canvas_width(&metrics, &spacing);
assert!(width > 0);
}
#[test]
fn test_is_dense() {
let sparse = make_test_graph(4, 4);
let sparse_metrics = DiagramMetrics::from_graph(&sparse);
assert!(!sparse_metrics.is_dense());
let dense = make_test_graph(20, 2);
let dense_metrics = DiagramMetrics::from_graph(&dense);
assert!(dense_metrics.is_dense());
}
}