git_graph/print/
svg.rs

1//! Create graphs in SVG format (Scalable Vector Graphics).
2
3use crate::graph::GitGraph;
4use crate::settings::Settings;
5use svg::node::element::path::Data;
6use svg::node::element::{Circle, Line, Path};
7use svg::Document;
8
9/// Creates a SVG visual representation of a graph.
10pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String> {
11    let mut document = Document::new();
12
13    let max_idx = graph.commits.len();
14    let mut max_column = 0;
15
16    if settings.debug {
17        for branch in &graph.all_branches {
18            if let (Some(start), Some(end)) = branch.range {
19                document = document.add(bold_line(
20                    start,
21                    branch.visual.column.unwrap(),
22                    end,
23                    branch.visual.column.unwrap(),
24                    "cyan",
25                ));
26            }
27        }
28    }
29
30    for (idx, info) in graph.commits.iter().enumerate() {
31        if let Some(trace) = info.branch_trace {
32            let branch = &graph.all_branches[trace];
33            let branch_color = &branch.visual.svg_color;
34
35            if branch.visual.column.unwrap() > max_column {
36                max_column = branch.visual.column.unwrap();
37            }
38
39            for p in 0..2 {
40                if let Some(par_oid) = info.parents[p] {
41                    if let Some(par_idx) = graph.indices.get(&par_oid) {
42                        let par_info = &graph.commits[*par_idx];
43                        let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
44
45                        let color = if info.is_merge {
46                            &par_branch.visual.svg_color
47                        } else {
48                            branch_color
49                        };
50
51                        if branch.visual.column == par_branch.visual.column {
52                            document = document.add(line(
53                                idx,
54                                branch.visual.column.unwrap(),
55                                *par_idx,
56                                par_branch.visual.column.unwrap(),
57                                color,
58                            ));
59                        } else {
60                            let split_index = super::get_deviate_index(graph, idx, *par_idx);
61                            document = document.add(path(
62                                idx,
63                                branch.visual.column.unwrap(),
64                                *par_idx,
65                                par_branch.visual.column.unwrap(),
66                                split_index,
67                                color,
68                            ));
69                        }
70                    }
71                }
72            }
73
74            document = document.add(commit_dot(
75                idx,
76                branch.visual.column.unwrap(),
77                branch_color,
78                !info.is_merge,
79            ));
80        }
81    }
82    let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1);
83    document = document
84        .set("viewBox", (0, 0, x_max, y_max))
85        .set("width", x_max)
86        .set("height", y_max);
87
88    let mut out: Vec<u8> = vec![];
89    svg::write(&mut out, &document).map_err(|err| err.to_string())?;
90    Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string()))
91}
92
93fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle {
94    let (x, y) = commit_coord(index, column);
95    Circle::new()
96        .set("cx", x)
97        .set("cy", y)
98        .set("r", 4)
99        .set("fill", if filled { color } else { "white" })
100        .set("stroke", color)
101        .set("stroke-width", 1)
102}
103
104fn line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line {
105    let (x1, y1) = commit_coord(index1, column1);
106    let (x2, y2) = commit_coord(index2, column2);
107    Line::new()
108        .set("x1", x1)
109        .set("y1", y1)
110        .set("x2", x2)
111        .set("y2", y2)
112        .set("stroke", color)
113        .set("stroke-width", 1)
114}
115
116fn bold_line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line {
117    let (x1, y1) = commit_coord(index1, column1);
118    let (x2, y2) = commit_coord(index2, column2);
119    Line::new()
120        .set("x1", x1)
121        .set("y1", y1)
122        .set("x2", x2)
123        .set("y2", y2)
124        .set("stroke", color)
125        .set("stroke-width", 5)
126}
127
128fn path(
129    index1: usize,
130    column1: usize,
131    index2: usize,
132    column2: usize,
133    split_idx: usize,
134    color: &str,
135) -> Path {
136    let c0 = commit_coord(index1, column1);
137
138    let c1 = commit_coord(split_idx, column1);
139    let c2 = commit_coord(split_idx + 1, column2);
140
141    let c3 = commit_coord(index2, column2);
142
143    let m = (0.5 * (c1.0 + c2.0), 0.5 * (c1.1 + c2.1));
144
145    let data = Data::new()
146        .move_to(c0)
147        .line_to(c1)
148        .quadratic_curve_to((c1.0, m.1, m.0, m.1))
149        .quadratic_curve_to((c2.0, m.1, c2.0, c2.1))
150        .line_to(c3);
151
152    Path::new()
153        .set("d", data)
154        .set("fill", "none")
155        .set("stroke", color)
156        .set("stroke-width", 1)
157}
158
159fn commit_coord(index: usize, column: usize) -> (f32, f32) {
160    (15.0 * (column as f32 + 1.0), 15.0 * (index as f32 + 1.0))
161}