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,
}
#[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>,
}
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(),
}
}
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
}
}