Lines
85.79 %
Functions
11.76 %
Branches
100 %
use std::cmp::Ordering;
use color_eyre::Result;
use petgraph::algo;
use petgraph::{graph::NodeIndex, Direction, Graph};
use svg::node::element::path::Data;
use svg::node::element::Element;
use svg::node::element::Path;
use svg::node::Text;
use svg::Document;
use svg::Node;
use crate::args::GraphArgs;
use crate::structs::{HapVariant, HaploNode, PhasedMatrix};
#[derive(Clone, Debug)]
pub struct Point {
text: String,
y: f32,
x: f32,
}
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let line = format!("Point {{ x: {}, y: {} }}", self.x, self.y);
write!(f, "{line}")
#[derive(Debug)]
pub struct DecayGraph<'a> {
pub g: &'a Graph<HaploNode, i64>,
pub vcf: &'a PhasedMatrix,
pub s: GraphArgs,
pub document: Document,
pub tree_height: f32,
// Basepairs per pixel
pub scale: f32,
pub nsamples: usize,
pub top_padding: f32,
pub side_padding: f32,
pub font_size: f32,
pub largest: usize,
pub variables: Option<Vec<String>>,
impl<'a> DecayGraph<'a> {
pub fn new(g: &'a Graph<HaploNode, i64>, vcf: &'a PhasedMatrix, s: GraphArgs) -> Self {
let mut largest = 0;
for idx in g.node_indices() {
let node = g.node_weight(idx).unwrap();
// let neighbors = g.neighbors_directed(idx, Direction::Outgoing);
if node.pos > largest {
largest = node.pos;
let nsamples = g.node_weight(NodeIndex::new(0)).unwrap().indexes.len();
let top_padding = s.height as f32 * 0.02;
let btm_padding = s.height as f32 * 0.05;
let side_padding = 0.02;
let document = Document::new()
.set("viewBox", (0, 0, s.width, s.height))
.set("style", format!("background-color:{}", s.background_color));
Self {
document,
tree_height: s.height as f32 - top_padding - btm_padding,
g,
vcf,
scale: largest as f32 / (s.height as f32 - top_padding - btm_padding),
font_size: s.font_size as f32,
s,
nsamples,
top_padding,
side_padding,
largest,
variables: None,
fn travel_majority(&self, parent_idx: NodeIndex) -> Option<NodeIndex> {
let nodes = self
.g
.neighbors_directed(parent_idx, Direction::Outgoing)
.collect::<Vec<NodeIndex>>();
if nodes.len() == 2 {
let c1 = self.g.node_weight(nodes[0]).unwrap();
let c2 = self.g.node_weight(nodes[1]).unwrap();
// Majority always to the left
let left_index = match c1.indexes_len.cmp(&c2.indexes_len) {
Ordering::Greater => nodes[0],
Ordering::Less => nodes[1],
Ordering::Equal => {
// Pick the node with gt==1
if let Some(last1) = c1.haplotype.iter().last() {
if last1.gt == 1 {
nodes[0]
} else {
nodes[1]
// This is a random choice as we cant do much better here anymore
},
};
self.travel_majority(left_index)
Some(parent_idx)
// Finds all the nodes in the majority branch
pub fn get_majority_nodes(&self) -> Option<Vec<NodeIndex>> {
let start_node = NodeIndex::new(0);
let end_node = self.travel_majority(start_node).unwrap();
algo::all_simple_paths(&self.g, start_node, end_node, 0, None).next()
pub fn find_ancestral_haplotype(&self) -> &Vec<HapVariant> {
let node_idx = self.travel_majority(NodeIndex::new(0)).unwrap();
let node = self.g.node_weight(node_idx).unwrap();
&node.haplotype
pub fn draw_graph(&mut self) -> Result<()> {
let root_idx = NodeIndex::new(0);
let width = (
self.s.width as f32 * self.side_padding,
self.s.width as f32 * (1.0 - self.side_padding),
);
// Draw once to find how much extra comes from font padding
let mut extra_y = self.pre_draw_recursion(root_idx, width, self.top_padding, 0.0);
extra_y *= 0.9;
let n_branch_data = 3;
extra_y += self.font_size * n_branch_data as f32;
// Rescale by subtracting the pixels lost to font scaling from the tree height
self.scale = (self.scale * self.tree_height) / (self.tree_height - extra_y);
let root_node = self.g.node_weight(root_idx).unwrap();
let root = Point {
text: root_node.indexes.len().to_string(),
x: self.s.width as f32 / 2.,
y: self.top_padding,
self.document
.append(get_text(&root, root_node, &self.s, 1.));
self.recursive_draw(root_idx, root, width, self.top_padding)?;
Ok(())
fn recursive_draw(
&mut self,
parent_idx: NodeIndex,
root: Point,
width: (f32, f32),
parent_y: f32,
) -> Result<()> {
let (start, end) = width;
let children: Vec<NodeIndex> = self
.collect();
if children.len() == 2 {
let parent = self.g.node_weight(parent_idx).unwrap();
let c1 = self.g.node_weight(children[0]).unwrap();
let c2 = self.g.node_weight(children[1]).unwrap();
let c1pos = (c1.pos - parent.pos) as f32;
let c2pos = (c2.pos - parent.pos) as f32;
// Majority always splits to the left, in equal cases gt==1 to the left
let (left_node, right_node, left_pos, right_pos, left_index, right_index) =
match c1.indexes_len.cmp(&c2.indexes_len) {
Ordering::Greater => (c1, c2, c1pos, c2pos, children[0], children[1]),
Ordering::Less => (c2, c1, c2pos, c1pos, children[1], children[0]),
(c1, c2, c1pos, c2pos, children[0], children[1])
(c2, c1, c2pos, c1pos, children[1], children[0])
let split_pos =
(end - start) * left_node.indexes_len as f32 / parent.indexes_len as f32;
let split = start + split_pos;
let x_left = start + split_pos / 2.;
let x_right = split + (end - split) / 2.;
let mut y_left = parent_y + left_pos / self.scale;
let mut y_right = parent_y + right_pos / self.scale;
if (y_left - parent_y) < self.font_size {
y_left += self.font_size - left_pos / self.scale;
y_right += self.font_size - left_pos / self.scale;
let left = Point {
text: left_node.indexes_len.to_string(),
x: x_left,
y: y_left,
let right = Point {
text: right_node.indexes_len.to_string(),
x: x_right,
y: y_right,
// let opacity = if left_node.indexes_len == 1 { 0.1 } else { 1. };
.append(get_text(&left, left_node, &self.s, 1.0));
.append(create_path(&root, &left, &self.s, 1.0));
let opacity = if right_node.indexes_len == 1 { 0.1 } else { 1. };
.append(get_text(&right, right_node, &self.s, opacity));
.append(create_path(&root, &right, &self.s, opacity));
// Draw data to the end of the branch
if left_node.indexes_len == 1 {
self.draw_branch_data(left.clone(), left_index)
self.recursive_draw(left_index, left, (start, split), y_left)?;
self.recursive_draw(right_index, right, (split, end), y_right)?;
fn draw_branch_data(&mut self, mut point: Point, end_idx: NodeIndex) {
let branch_end_node = self.g.node_weight(end_idx).unwrap();
let paths =
algo::all_simple_paths::<Vec<_>, _>(&self.g, NodeIndex::new(0), end_idx, 0, None)
.collect::<Vec<_>>();
let path = paths.first().unwrap();
let branch_start_node = self.first_branching_node(path, end_idx).unwrap().clone();
if branch_start_node.indexes_len > 5 || branch_end_node.pos == self.largest {
let n = branch_start_node.indexes_len.to_string();
// Branch end position
let pos = branch_end_node.pos;
point.y += self.font_size;
point.text = format!("{:.2} mb", pos as f32 / 1_000_000.);
.append(branch_data(&point, &self.s, 1.0, "#ff2f8e"));
// Number of branch samples
point.text = format!("n={n}");
.append(branch_data(&point, &self.s, 1.0, "#66df48"));
if let Some(variables) = &self.variables {
for var in variables {
let v = self
.vcf
.get_variable_data_mean(&branch_start_node.indexes, &[var.to_owned()])
.unwrap();
// Age of onset mean
if let Some(var) = v {
point.text = format!("{:.2} y", var[0]);
.append(branch_data(&point, &self.s, 1.0, "#6a77dd"));
fn first_branching_node(&self, path: &[NodeIndex], end_idx: NodeIndex) -> Option<&HaploNode> {
let mut prev_node = self.g.node_weight(end_idx).unwrap();
for idx in path.iter().rev() {
let curr_node = self.g.node_weight(*idx).unwrap();
if curr_node.indexes_len as f32 / 2. > prev_node.indexes_len as f32 {
return Some(prev_node);
prev_node = curr_node;
Some(self.g.node_weight(NodeIndex::new(0)).unwrap())
fn pre_draw_recursion(
node_idx: NodeIndex,
mut extra_y: f32,
) -> f32 {
.neighbors_directed(node_idx, Direction::Outgoing)
let parent = self.g.node_weight(node_idx).unwrap();
let (left_node, _, left_pos, _, left_index, _) = if c1.indexes_len > c2.indexes_len {
let y_left = parent_y + left_pos / self.scale;
// This means the length of the shared sequence is smaller than font size and thus
// padding is required
extra_y += self.font_size - left_pos / self.scale;
// Effectively all the font scaling comes from the majority branch,
// so recursion to the right is skipped
self.pre_draw_recursion(left_index, (start, split), y_left, extra_y)
extra_y
pub fn get_text(p: &Point, n: &HaploNode, s: &GraphArgs, o: f32) -> Element {
let (color, o) = match n.decoys.is_some() && n.indexes_len == 1 {
true => ("red", 1.),
false => (s.color.as_str(), o),
let mut element = Element::new("text");
element.assign("x", p.x);
element.assign("y", p.y);
element.assign("opacity", o);
element.assign("font-weight", "bold");
element.assign("fill", color);
element.assign("font-size", format!("{}px", s.font_size));
element.append(Text::new(p.text.clone()));
element
pub fn branch_data(p: &Point, s: &GraphArgs, o: f32, color: &str) -> Element {
element.assign("font-size", format!("{}px", s.font_size as f32 * 0.7));
pub fn create_path(parent: &Point, child: &Point, s: &GraphArgs, o: f32) -> Path {
let font_size = s.font_size as f32;
let x_start = match parent.text.chars().count() {
4 => font_size * 2.5,
3 => font_size * 1.9,
2 => font_size * 1.15,
1 => font_size * 0.5,
_ => font_size * 3.0,
let shorten_path = if parent.x > child.x {
match child.text.chars().count() {
2 => font_size * 1.25,
1 => font_size * 0.7,
4 => font_size * -0.74,
3 => font_size * -0.54,
2 => font_size * -0.32,
1 => font_size * -0.10,
_ => font_size * -1.0,
let data = Data::new()
.move_to((parent.x + x_start, parent.y + font_size * 0.18))
.line_to((parent.x + x_start, child.y - font_size * 0.2))
.line_to((child.x + shorten_path, child.y - font_size * 0.2));
Path::new()
.set("fill", "none")
.set("stroke", s.color.as_str())
.set("opacity", o)
.set("stroke-width", s.stroke_width)
.set("d", data)
#[cfg(test)]
#[rustfmt::skip]
mod tests {
use super::*;
#[test]
fn point_display() {
let point = Point { x: 5.0, y: 6.0, text: "".to_string() };
assert_eq!("Point { x: 5, y: 6 }".to_string(), format!("{point}"));