use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
LeftToRight,
TopToBottom,
RightToLeft,
BottomToTop,
}
impl Direction {
pub fn parse(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"LR" => Some(Self::LeftToRight),
"TD" | "TB" => Some(Self::TopToBottom),
"RL" => Some(Self::RightToLeft),
"BT" => Some(Self::BottomToTop),
_ => None,
}
}
pub fn is_horizontal(self) -> bool {
matches!(self, Self::LeftToRight | Self::RightToLeft)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NodeShape {
#[default]
Rectangle,
Rounded,
Diamond,
Circle,
Stadium,
Subroutine,
Cylinder,
Hexagon,
Asymmetric,
Parallelogram,
Trapezoid,
DoubleCircle,
Bar(BarOrientation),
Note,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarOrientation {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub fn parse_hex(s: &str) -> Option<Self> {
let body = s.strip_prefix('#')?;
match body.len() {
3 => {
let r = u8::from_str_radix(&body[0..1], 16).ok()?;
let g = u8::from_str_radix(&body[1..2], 16).ok()?;
let b = u8::from_str_radix(&body[2..3], 16).ok()?;
Some(Self(r * 0x11, g * 0x11, b * 0x11))
}
6 => {
let r = u8::from_str_radix(&body[0..2], 16).ok()?;
let g = u8::from_str_radix(&body[2..4], 16).ok()?;
let b = u8::from_str_radix(&body[4..6], 16).ok()?;
Some(Self(r, g, b))
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct NodeStyle {
pub fill: Option<Rgb>,
pub stroke: Option<Rgb>,
pub color: Option<Rgb>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EdgeStyleColors {
pub stroke: Option<Rgb>,
pub color: Option<Rgb>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EdgeStyle {
#[default]
Solid,
Dotted,
Thick,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EdgeEndpoint {
#[default]
Arrow,
None,
Circle,
Cross,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
pub id: String,
pub label: String,
pub shape: NodeShape,
}
impl Node {
pub fn new(id: impl Into<String>, label: impl Into<String>, shape: NodeShape) -> Self {
Self {
id: id.into(),
label: label.into(),
shape,
}
}
pub fn label_width(&self) -> usize {
use unicode_width::UnicodeWidthStr;
self.label
.lines()
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0)
}
pub fn label_line_count(&self) -> usize {
let n = self.label.lines().count();
n.max(1)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edge {
pub from: String,
pub to: String,
pub label: Option<String>,
pub style: EdgeStyle,
pub end: EdgeEndpoint,
pub start: EdgeEndpoint,
}
impl Edge {
pub fn new(from: impl Into<String>, to: impl Into<String>, label: Option<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
label,
style: EdgeStyle::Solid,
end: EdgeEndpoint::Arrow,
start: EdgeEndpoint::None,
}
}
pub fn new_styled(
from: impl Into<String>,
to: impl Into<String>,
label: Option<String>,
style: EdgeStyle,
start: EdgeEndpoint,
end: EdgeEndpoint,
) -> Self {
Self {
from: from.into(),
to: to.into(),
label,
style,
end,
start,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subgraph {
pub id: String,
pub label: String,
pub direction: Option<Direction>,
pub node_ids: Vec<String>,
pub subgraph_ids: Vec<String>,
}
impl Subgraph {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
let id = id.into();
let label = label.into();
Self {
id,
label,
direction: None,
node_ids: Vec::new(),
subgraph_ids: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Graph {
pub direction: Direction,
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub subgraphs: Vec<Subgraph>,
pub node_styles: HashMap<String, NodeStyle>,
pub edge_styles: HashMap<usize, EdgeStyleColors>,
pub class_defs: HashMap<String, NodeStyle>,
pub subgraph_styles: HashMap<String, NodeStyle>,
}
impl Graph {
pub fn new(direction: Direction) -> Self {
Self {
direction,
nodes: Vec::new(),
edges: Vec::new(),
subgraphs: Vec::new(),
node_styles: HashMap::new(),
edge_styles: HashMap::new(),
class_defs: HashMap::new(),
subgraph_styles: HashMap::new(),
}
}
pub fn node(&self, id: &str) -> Option<&Node> {
self.nodes.iter().find(|n| n.id == id)
}
pub fn has_node(&self, id: &str) -> bool {
self.nodes.iter().any(|n| n.id == id)
}
pub fn upsert_node(&mut self, node: Node) {
if let Some(existing) = self.nodes.iter_mut().find(|n| n.id == node.id) {
if existing.label == existing.id
&& (existing.shape == NodeShape::Rectangle)
&& (node.label != node.id || node.shape != NodeShape::Rectangle)
{
*existing = node;
}
} else {
self.nodes.push(node);
}
}
pub fn node_to_subgraph(&self) -> HashMap<String, String> {
let mut map = HashMap::new();
for sg in &self.subgraphs {
self.collect_node_subgraph_map(sg, &mut map);
}
map
}
fn collect_node_subgraph_map(&self, sg: &Subgraph, map: &mut HashMap<String, String>) {
for nid in &sg.node_ids {
map.insert(nid.clone(), sg.id.clone());
}
for child_id in &sg.subgraph_ids {
if let Some(child) = self.find_subgraph(child_id) {
self.collect_node_subgraph_map(child, map);
}
}
}
pub fn find_subgraph(&self, id: &str) -> Option<&Subgraph> {
fn search<'a>(sgs: &'a [Subgraph], all: &'a [Subgraph], id: &str) -> Option<&'a Subgraph> {
for sg in sgs {
if sg.id == id {
return Some(sg);
}
for child_id in &sg.subgraph_ids {
if let Some(found) = all.iter().find(|s| &s.id == child_id)
&& let Some(result) = search(std::slice::from_ref(found), all, id)
{
return Some(result);
}
}
}
None
}
search(&self.subgraphs, &self.subgraphs, id)
}
pub fn all_nodes_in_subgraph(&self, sg: &Subgraph) -> Vec<String> {
let mut result = sg.node_ids.clone();
for child_id in &sg.subgraph_ids {
if let Some(child) = self.find_subgraph(child_id) {
result.extend(self.all_nodes_in_subgraph(child));
}
}
result
}
pub fn parallel_edge_groups(&self) -> Vec<Vec<usize>> {
let mut groups: std::collections::BTreeMap<(String, String), Vec<usize>> =
std::collections::BTreeMap::new();
for (idx, edge) in self.edges.iter().enumerate() {
let key = if edge.from <= edge.to {
(edge.from.clone(), edge.to.clone())
} else {
(edge.to.clone(), edge.from.clone())
};
groups.entry(key).or_default().push(idx);
}
let mut out: Vec<Vec<usize>> = groups
.into_values()
.filter(|indices| indices.len() >= 2)
.collect();
out.sort_by_key(|g| g[0]);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(id: &str) -> Node {
Node::new(id, id, NodeShape::Rectangle)
}
fn graph_with_edges(edges: &[(&str, &str)]) -> Graph {
let mut g = Graph::new(Direction::LeftToRight);
let mut seen = std::collections::HashSet::new();
for (from, to) in edges {
for id in [from, to] {
if seen.insert(*id) {
g.nodes.push(rect(id));
}
}
g.edges.push(Edge::new(*from, *to, None));
}
g
}
#[test]
fn parallel_groups_empty_for_single_edge() {
let g = graph_with_edges(&[("A", "B")]);
assert!(g.parallel_edge_groups().is_empty());
}
#[test]
fn parallel_groups_empty_for_unrelated_edges() {
let g = graph_with_edges(&[("A", "B"), ("C", "D"), ("E", "F")]);
assert!(g.parallel_edge_groups().is_empty());
}
#[test]
fn parallel_groups_detects_two_same_direction() {
let g = graph_with_edges(&[("T", "D"), ("T", "D")]);
let groups = g.parallel_edge_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0], vec![0, 1]);
}
#[test]
fn parallel_groups_detects_bidirectional_pair() {
let g = graph_with_edges(&[("F", "W"), ("W", "F")]);
let groups = g.parallel_edge_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0], vec![0, 1]);
}
#[test]
fn parallel_groups_detects_three_between_same_pair() {
let g = graph_with_edges(&[("A", "B"), ("A", "B"), ("B", "A")]);
let groups = g.parallel_edge_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0], vec![0, 1, 2]);
}
#[test]
fn parallel_groups_separates_distinct_pairs() {
let g = graph_with_edges(&[
("A", "B"),
("C", "D"),
("A", "B"), ("C", "D"), ]);
let groups = g.parallel_edge_groups();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0], vec![0, 2]);
assert_eq!(groups[1], vec![1, 3]);
}
#[test]
fn parallel_groups_self_loop_alone_excluded() {
let g = graph_with_edges(&[("A", "A")]);
assert!(g.parallel_edge_groups().is_empty());
}
#[test]
fn parallel_groups_multiple_self_loops_grouped() {
let g = graph_with_edges(&[("A", "A"), ("A", "A")]);
let groups = g.parallel_edge_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0], vec![0, 1]);
}
#[test]
fn parallel_groups_returned_in_source_order() {
let g = graph_with_edges(&[
("X", "Y"),
("A", "B"),
("X", "Y"), ("A", "B"), ]);
let groups = g.parallel_edge_groups();
assert_eq!(groups[0][0], 0); assert_eq!(groups[1][0], 1); }
}