Skip to content

Commit 341f5a5

Browse files
committed
Added details to SVG graph
1 parent 87b4473 commit 341f5a5

1 file changed

Lines changed: 161 additions & 21 deletions

File tree

src/print/svg.rs

Lines changed: 161 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
//! Create graphs in SVG format (Scalable Vector Graphics).
22
3+
use crate::graph::CommitInfo;
34
use crate::graph::GitGraph;
45
use crate::settings::Settings;
56
use svg::node::element::path::Data;
6-
use svg::node::element::{Circle, Line, Path};
7+
use svg::node::element::{Circle, Group, Line, Path, Text, Title};
78
use svg::Document;
89

910
/// Creates a SVG visual representation of a graph.
1011
pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String> {
1112
let mut document = Document::new();
1213

1314
let max_idx = graph.commits.len();
14-
let mut max_column = 0;
15+
let mut widest_summary = 0.0;
16+
let mut widest_branch_names = 0.0;
1517

1618
if settings.debug {
1719
for branch in &graph.all_branches {
@@ -27,15 +29,21 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String
2729
}
2830
}
2931

32+
let max_column = graph
33+
.commits
34+
.iter()
35+
.filter_map(|info| {
36+
info.branch_trace
37+
.and_then(|trace| graph.all_branches[trace].visual.column)
38+
})
39+
.max()
40+
.unwrap_or(0);
41+
3042
for (idx, info) in graph.commits.iter().enumerate() {
3143
if let Some(trace) = info.branch_trace {
3244
let branch = &graph.all_branches[trace];
3345
let branch_color = &branch.visual.svg_color;
3446

35-
if branch.visual.column.unwrap() > max_column {
36-
max_column = branch.visual.column.unwrap();
37-
}
38-
3947
for p in 0..2 {
4048
let parent = info.parents[p];
4149
let Some(par_oid) = parent else {
@@ -84,19 +92,53 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String
8492
}
8593
}
8694

87-
document = document.add(commit_dot(
88-
idx,
89-
branch.visual.column.unwrap(),
90-
branch_color,
91-
!info.is_merge,
92-
));
95+
document = document.add(
96+
commit_dot(
97+
idx,
98+
branch.visual.column.unwrap(),
99+
branch_color,
100+
!info.is_merge,
101+
)
102+
.add(Title::new(&info.oid.to_string())),
103+
);
104+
105+
let commit = graph
106+
.repository
107+
.find_commit(info.oid)
108+
.map_err(|err| err.message().to_string())?;
109+
110+
let commit_str = commit.summary().unwrap_or("");
111+
112+
document = document.add(draw_summary(idx, max_column, &commit_str));
113+
114+
match draw_branches(idx, branch.visual.column.unwrap(), info, graph) {
115+
Some((branches, width)) => {
116+
document = document.add(branches);
117+
118+
widest_branch_names = f32::max(widest_branch_names, width);
119+
}
120+
None => {}
121+
}
122+
123+
widest_summary = f32::max(widest_summary, text_bounding_box(&commit_str, 12.0).0);
93124
}
94125
}
126+
95127
let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1);
128+
96129
document = document
97-
.set("viewBox", (0, 0, x_max, y_max))
98-
.set("width", x_max)
99-
.set("height", y_max);
130+
.set(
131+
"viewBox",
132+
(
133+
-widest_branch_names,
134+
0,
135+
x_max + widest_branch_names + widest_summary,
136+
y_max,
137+
),
138+
)
139+
.set("width", x_max + widest_branch_names + widest_summary + 15.0)
140+
.set("height", y_max)
141+
.set("style", "font-family:monospace;font-size:12px;");
100142

101143
let mut out: Vec<u8> = vec![];
102144
svg::write(&mut out, &document).map_err(|err| err.to_string())?;
@@ -114,6 +156,97 @@ fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle
114156
.set("stroke-width", 1)
115157
}
116158

159+
fn draw_branches(
160+
index: usize,
161+
column: usize,
162+
info: &CommitInfo,
163+
graph: &GitGraph,
164+
) -> Option<(Group, f32)> {
165+
let (x, y) = commit_coord(index, column);
166+
167+
let mut branch_names = info
168+
.branches
169+
.iter()
170+
.map(|b| graph.all_branches[*b].name.clone())
171+
.collect::<Vec<String>>();
172+
173+
if graph.head.oid == info.oid {
174+
// Head is here
175+
match branch_names
176+
.iter()
177+
.position(|name| name == &graph.head.name)
178+
{
179+
Some(index) => {
180+
branch_names.insert(index + 1, "HEAD".to_string());
181+
}
182+
//Detached HEAD
183+
None => branch_names.push("HEAD".to_string()),
184+
}
185+
}
186+
187+
if branch_names.len() > 0 {
188+
let mut g = Group::new();
189+
let mut start: f32 = 5.0;
190+
191+
for branch_name in &branch_names {
192+
let gap = 9.0
193+
+ if branch_name == "HEAD" && graph.head.is_branch {
194+
0.0
195+
} else {
196+
8.0
197+
};
198+
g = g.add(draw_branch(start - gap, 2.5, branch_name));
199+
200+
start = start - text_bounding_box(&branch_name, 12.0).0 - gap;
201+
}
202+
203+
g = g.set("transform", format!("translate({x}, {y})"));
204+
205+
Some((g.clone(), -(start + x)))
206+
} else {
207+
None
208+
}
209+
}
210+
211+
fn draw_branch(x: f32, y: f32, branch_name: &String) -> Group {
212+
let width = text_bounding_box(&branch_name, 12.0).0;
213+
214+
Group::new()
215+
.add(Text::new(branch_name).set("x", x - width).set("y", y + 1.0))
216+
.add(
217+
Path::new()
218+
.set(
219+
"d",
220+
Data::new()
221+
//Tip
222+
.move_to((x + 2.0, y + 4.0))
223+
.line_by((6.0, -7.0))
224+
.line_by((-6.0, -7.0))
225+
//Body
226+
.horizontal_line_by(-width - 11.0)
227+
//Rear
228+
.line_by((6.0, 7.0))
229+
.line_by((-6.0, 7.0))
230+
.close(),
231+
)
232+
.set("stroke", "#00000000")
233+
.set("fill", "#00000030"),
234+
)
235+
}
236+
237+
fn draw_summary(index: usize, max_column: usize, hash: &str) -> Text {
238+
let (x, y) = commit_coord(index, max_column);
239+
Text::new(hash)
240+
.set("x", x + 15.0)
241+
.set("y", y + 2.0)
242+
.set("style", "font-family:monospace;font-size:12px")
243+
}
244+
245+
fn text_bounding_box(text: &str, size: f32) -> (f32, f32) {
246+
// Let's assume the font has a 60% width
247+
(text.len() as f32 * size * 0.6, size)
248+
}
249+
117250
fn line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line {
118251
let (x1, y1) = commit_coord(index1, column1);
119252
let (x2, y2) = commit_coord(index2, column2);
@@ -155,12 +288,19 @@ fn path(
155288

156289
let m = (0.5 * (c1.0 + c2.0), 0.5 * (c1.1 + c2.1));
157290

158-
let data = Data::new()
159-
.move_to(c0)
160-
.line_to(c1)
161-
.quadratic_curve_to((c1.0, m.1, m.0, m.1))
162-
.quadratic_curve_to((c2.0, m.1, c2.0, c2.1))
163-
.line_to(c3);
291+
let data = if column2 > column1 {
292+
Data::new()
293+
.move_to(c0)
294+
.line_to(c1)
295+
.line_to((c2.0, m.1))
296+
.line_to(c3)
297+
} else {
298+
Data::new()
299+
.move_to(c0)
300+
.line_to((c1.0, m.1))
301+
.line_to(c2)
302+
.line_to(c3)
303+
};
164304

165305
Path::new()
166306
.set("d", data)

0 commit comments

Comments
 (0)