fn render_graphml(graph: &SimpleGraph) -> Result<String> {
let mut graphml = String::new();
write_graphml_header(&mut graphml)?;
write_graphml_nodes(&mut graphml, graph)?;
write_graphml_edges(&mut graphml, graph)?;
write_graphml_footer(&mut graphml)?;
Ok(graphml)
}
fn escape_xml(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}
fn write_graphml_header(graphml: &mut String) -> Result<()> {
use std::fmt::Write;
writeln!(graphml, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
graphml,
r#"<graphml xmlns="http://graphml.graphdrawing.org/xmlns">"#
)?;
writeln!(
graphml,
r#" <key id="d0" for="node" attr.name="label" attr.type="string"/>"#
)?;
writeln!(graphml, r#" <graph id="G" edgedefault="directed">"#)?;
Ok(())
}
fn write_graphml_nodes(graphml: &mut String, graph: &SimpleGraph) -> Result<()> {
use std::fmt::Write;
for idx in graph.node_indices() {
writeln!(
graphml,
r#" <node id="n{}"><data key="d0">{}</data></node>"#,
idx.index(),
escape_xml(graph.get_node(idx))
)?;
}
Ok(())
}
fn write_graphml_edges(graphml: &mut String, graph: &SimpleGraph) -> Result<()> {
use std::fmt::Write;
for (source, target) in graph.edge_endpoints() {
writeln!(
graphml,
r#" <edge source="n{}" target="n{}" />"#,
source.index(),
target.index()
)?;
}
Ok(())
}
fn write_graphml_footer(graphml: &mut String) -> Result<()> {
use std::fmt::Write;
writeln!(graphml, " </graph>")?;
writeln!(graphml, "</graphml>")?;
Ok(())
}
fn fmt_metric(value: Option<f64>) -> String {
value.map_or_else(|| "n/a".to_string(), |v| format!("{v:.3}"))
}
fn csv_metric(value: Option<f64>) -> String {
value.map_or_else(String::new, |v| format!("{v:.3}"))
}
fn fmt_component(value: Option<usize>) -> String {
value.map_or_else(|| "n/a".to_string(), |v| v.to_string())
}
fn format_output(
result: GraphMetricsResult,
format: crate::cli::GraphMetricsOutputFormat,
graph: &SimpleGraph,
) -> Result<String> {
match format {
crate::cli::GraphMetricsOutputFormat::Json => format_gm_as_json(result),
crate::cli::GraphMetricsOutputFormat::Summary => format_gm_as_summary(&result),
crate::cli::GraphMetricsOutputFormat::Human => format_gm_as_human(result),
crate::cli::GraphMetricsOutputFormat::Detailed => format_gm_as_detailed(&result),
crate::cli::GraphMetricsOutputFormat::Csv => format_gm_as_csv(result),
crate::cli::GraphMetricsOutputFormat::GraphML => render_graphml(graph),
crate::cli::GraphMetricsOutputFormat::Markdown => format_gm_as_markdown(result),
}
}
fn format_gm_as_json(result: GraphMetricsResult) -> Result<String> {
Ok(serde_json::to_string_pretty(&result)?)
}
fn format_gm_as_human(result: GraphMetricsResult) -> Result<String> {
let mut output = String::new();
write_gm_human_header(&mut output)?;
write_gm_statistics(&mut output, &result)?;
write_gm_top_nodes(&mut output, &result)?;
Ok(output)
}
fn format_gm_as_summary(result: &GraphMetricsResult) -> Result<String> {
let mut output = String::new();
write_gm_human_header(&mut output)?;
write_gm_statistics(&mut output, result)?;
Ok(output)
}
fn format_gm_as_detailed(result: &GraphMetricsResult) -> Result<String> {
let mut output = String::new();
write_gm_human_header(&mut output)?;
write_gm_statistics(&mut output, result)?;
write_gm_top_nodes(&mut output, result)?;
write_gm_rankings(&mut output, result)?;
Ok(output)
}
type CentralityMeasure = (&'static str, fn(&NodeMetrics) -> Option<f64>);
fn write_gm_rankings(output: &mut String, result: &GraphMetricsResult) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
let measures: [CentralityMeasure; 5] = [
("PageRank", |n| n.pagerank),
("Betweenness", |n| n.betweenness_centrality),
("Closeness", |n| n.closeness_centrality),
("Clustering", |n| n.clustering_coefficient),
("Degree", |n| Some(n.degree_centrality)),
];
for (label, key) in measures {
writeln!(output, "\n{}Ranked by {}{}\n", c::BOLD, label, c::RESET)?;
if result.nodes.iter().all(|n| key(n).is_none()) {
writeln!(
output,
" not computed — add it to --metrics (or use --metrics all)"
)?;
continue;
}
let mut ranked: Vec<&NodeMetrics> = result.nodes.iter().collect();
ranked.sort_by(|a, b| {
key(b)
.unwrap_or(f64::NEG_INFINITY)
.total_cmp(&key(a).unwrap_or(f64::NEG_INFINITY))
.then_with(|| a.name.cmp(&b.name))
});
for (i, node) in ranked.iter().enumerate() {
writeln!(
output,
" {}. {}{}{} {}{}{}",
i + 1,
c::CYAN,
node.name,
c::RESET,
c::BOLD_WHITE,
fmt_metric(key(node)),
c::RESET
)?;
}
}
Ok(())
}
fn write_gm_human_header(output: &mut String) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(
output,
"{}{}Graph Metrics Analysis{}\n",
c::BOLD,
c::UNDERLINE,
c::RESET
)?;
writeln!(output, "{}Graph Statistics{}", c::BOLD, c::RESET)?;
Ok(())
}
fn write_gm_statistics(output: &mut String, result: &GraphMetricsResult) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(
output,
" {}Total nodes:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
result.total_nodes,
c::RESET
)?;
writeln!(
output,
" {}Total edges:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
result.total_edges,
c::RESET
)?;
writeln!(
output,
" {}Density:{} {}{:.3}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
result.density,
c::RESET
)?;
writeln!(
output,
" {}Average degree:{} {}{:.2}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
result.average_degree,
c::RESET
)?;
writeln!(
output,
" {}Max degree:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
result.max_degree,
c::RESET
)?;
writeln!(
output,
" {}Connected components:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
result.connected_components,
c::RESET
)?;
Ok(())
}
fn write_gm_top_nodes(output: &mut String, result: &GraphMetricsResult) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, "\n{}Top Nodes by Centrality{}\n", c::BOLD, c::RESET)?;
for (i, node) in result.nodes.iter().enumerate() {
write_gm_node_details(output, i + 1, node)?;
}
Ok(())
}
fn write_gm_node_details(output: &mut String, index: usize, node: &NodeMetrics) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, " {}. {}{}{}", index, c::CYAN, node.name, c::RESET)?;
writeln!(
output,
" {}Degree:{} {}{:.3}{} (in: {}, out: {})",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
node.degree_centrality,
c::RESET,
node.in_degree,
node.out_degree
)?;
writeln!(
output,
" {}Betweenness:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
fmt_metric(node.betweenness_centrality),
c::RESET
)?;
writeln!(
output,
" {}Closeness:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
fmt_metric(node.closeness_centrality),
c::RESET
)?;
writeln!(
output,
" {}PageRank:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
fmt_metric(node.pagerank),
c::RESET
)?;
writeln!(
output,
" {}Clustering:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
fmt_metric(node.clustering_coefficient),
c::RESET
)?;
writeln!(
output,
" {}Component:{} {}{}{}",
c::BOLD,
c::RESET,
c::BOLD_WHITE,
fmt_component(node.component_id),
c::RESET
)?;
writeln!(output)?;
Ok(())
}
fn format_gm_as_csv(result: GraphMetricsResult) -> Result<String> {
use std::fmt::Write;
let mut output = String::new();
writeln!(
output,
"name,degree_centrality,betweenness,closeness,pagerank,clustering,component_id,in_degree,out_degree"
)?;
for node in result.nodes {
writeln!(
output,
"{},{:.3},{},{},{},{},{},{},{}",
node.name,
node.degree_centrality,
csv_metric(node.betweenness_centrality),
csv_metric(node.closeness_centrality),
csv_metric(node.pagerank),
csv_metric(node.clustering_coefficient),
node.component_id
.map_or_else(String::new, |id| id.to_string()),
node.in_degree,
node.out_degree
)?;
}
Ok(output)
}
fn format_gm_as_markdown(result: GraphMetricsResult) -> Result<String> {
let mut output = String::new();
write_gm_markdown_header(&mut output)?;
write_gm_markdown_summary(&mut output, &result)?;
write_gm_markdown_top_nodes(&mut output, &result)?;
Ok(output)
}
fn write_gm_markdown_header(output: &mut String) -> Result<()> {
use std::fmt::Write;
writeln!(output, "# Graph Metrics Report\n")?;
writeln!(output, "## Summary\n")?;
Ok(())
}
fn write_gm_markdown_summary(output: &mut String, result: &GraphMetricsResult) -> Result<()> {
use std::fmt::Write;
writeln!(output, "| Metric | Value |")?;
writeln!(output, "|--------|-------|")?;
writeln!(output, "| Total Nodes | {} |", result.total_nodes)?;
writeln!(output, "| Total Edges | {} |", result.total_edges)?;
writeln!(output, "| Density | {:.3} |", result.density)?;
writeln!(output, "| Average Degree | {:.2} |", result.average_degree)?;
writeln!(output, "| Max Degree | {} |", result.max_degree)?;
writeln!(
output,
"| Connected Components | {} |",
result.connected_components
)?;
Ok(())
}
fn write_gm_markdown_top_nodes(output: &mut String, result: &GraphMetricsResult) -> Result<()> {
use std::fmt::Write;
writeln!(output, "\n## Top Nodes\n")?;
writeln!(
output,
"| Node | Degree | Betweenness | Closeness | PageRank | Clustering | Component |"
)?;
writeln!(
output,
"|------|--------|-------------|-----------|----------|------------|-----------|"
)?;
for node in result.nodes.iter().take(10) {
writeln!(
output,
"| {} | {:.3} | {} | {} | {} | {} | {} |",
node.name,
node.degree_centrality,
fmt_metric(node.betweenness_centrality),
fmt_metric(node.closeness_centrality),
fmt_metric(node.pagerank),
fmt_metric(node.clustering_coefficient),
fmt_component(node.component_id)
)?;
}
Ok(())
}