use calamine::{Data, DataType, Reader, Xls, open_workbook};
use graphviz_rust::{
cmd::{CommandArg, Format},
exec, parse,
};
use petgraph::Direction;
use petgraph::dot::Dot;
use petgraph::graph::NodeIndex;
use regex::Regex;
use std::{fs::File, io::Write, path::Path};
pub mod family_graph;
use family_graph::{AncestorHeader, FamilyGraph};
pub use family_graph::{D3Node, Person, Relationship};
pub use petgraph::graph;
pub fn create_dotviz(family: &FamilyGraph) -> std::io::Result<()> {
let fancy_dot = Dot::with_attr_getters(
&family.0,
&[],
&|_graph, edge_ref| {
match edge_ref.weight() {
Relationship::Child => "style=solid, color=black, penwidth=2".to_owned(),
Relationship::Married => "style=bold, color=red, penwidth=3".to_owned(),
Relationship::Divorced => "style=dashed, color=red, penwidth=2".to_owned(),
Relationship::Dating => "style=dotted, color=pink, penwidth=2".to_owned(),
Relationship::ChildFromPartner => {
"style=dashed, color=orange, penwidth=2".to_owned()
}
Relationship::Relative => "style=dashed, color=gray, penwidth=1".to_owned(),
}
},
&|_graph, node_ref| {
let person = node_ref.1; format!(
"label=\"{}\", shape=box, style=filled, fillcolor=lightblue",
person.name.replace("\"", "\\\"")
) },
);
let dot_string = format!("{}", fancy_dot);
match parse(&dot_string) {
Ok(parsed_graph) => {
let mut ctx = graphviz_rust::printer::PrinterContext::default();
match exec(
parsed_graph,
&mut ctx,
vec![CommandArg::Format(Format::Svg)],
) {
Ok(svg_bytes) => {
println!("SVG generated successfully!");
std::fs::write("family_graph.svg", &svg_bytes)?;
}
Err(e) => {
eprintln!("Error generating SVG: {}", e);
}
}
}
Err(e) => {
eprintln!("Error parsing DOT: {}", e);
}
}
Ok(())
}
pub fn create_d3_export(family: &FamilyGraph, export_path: &str) -> std::io::Result<Vec<D3Node>> {
let roots: Vec<NodeIndex> = family
.node_indices()
.filter(|&node_idx| {
!family
.edges_directed(node_idx, Direction::Incoming)
.any(|edge| matches!(edge.weight(), Relationship::Child))
})
.collect();
let mut tree_data: Vec<D3Node> = Vec::new();
for root_idx in roots {
let node_tree = family.build_subtree(root_idx);
tree_data.push(node_tree);
}
let json_string =
serde_json::to_string_pretty(&tree_data).expect("Failed to serialize tree data to JSON");
let js_content = format!("export const familytreeData = {};", json_string);
let mut file = File::create(export_path)?;
file.write_all(js_content.as_bytes())?;
println!("D3 data generated successfully: {export_path}");
Ok(tree_data)
}
pub fn run_grapher(path: &Path, sheet_name: &str) -> std::io::Result<FamilyGraph> {
let mut workbook: Xls<_> = open_workbook(path).expect("Cannot open file");
let range = workbook
.worksheet_range(sheet_name)
.expect("Cannot get worksheet");
let data_offet = 2;
let all_rows: Vec<_> = range.rows().collect();
let re = Regex::new(
r"af\s+(?P<n1>.+?)\s+(?P<d1>\d{4}-\d{4})\s+og\s+(?P<n2>.+?)\s+(?P<d2>\d{4}-\d{4})",
)
.unwrap();
let a1_cell = all_rows
.first()
.expect("Is sheet empty?")
.first()
.expect("No first column?");
let header_names = match re.captures(&a1_cell.to_string()) {
Some(caps) => Some(AncestorHeader {
name1: caps.name("n1").map_or("", |m| m.as_str()).to_string(),
years1: caps.name("d1").map_or("", |m| m.as_str()).to_string(),
name2: caps.name("n2").map_or("", |m| m.as_str()).to_string(),
years2: caps.name("d2").map_or("", |m| m.as_str()).to_string(),
}),
_ => {
println!("a1 cell: {:?}", a1_cell);
panic!("could not find names in first row")
}
};
fn cell_empty(r: &&[Data]) -> bool {
r.first().map_or_else(|| true, |cell| cell.is_empty())
}
let entries: Vec<Vec<_>> = match all_rows.len() {
len if len > 5 => all_rows[data_offet..len - 3]
.split(cell_empty)
.filter(|group| !group.is_empty())
.map(|group| group.to_vec())
.collect(),
_ => {
println!(
"Warning: Not enough rows to trim (need >5, got {})",
all_rows.len()
);
Vec::new()
}
};
let family_graph: FamilyGraph = FamilyGraph::create_family(entries, header_names);
Ok(family_graph)
}