diff --git a/src/main.rs b/src/main.rs index 2210e51..688db17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -221,7 +221,52 @@ async fn render( } } - let page = cache.pages.get(path.as_ref()); + // Special case - graph view + let mut graph_page = ParsedPage { + timestamp: None, + title: "".to_string(), + html: "".to_string(), + links: vec![], + }; + + let page = if path.as_str() != "!graph" { + cache.pages.get(path.as_ref()).clone() + } else { + let mut context = Context::new(); + + let mut nodes: Vec> = vec![]; + let mut links: Vec> = vec![]; + + let page_ids: Vec<&String> = cache.pages.keys().collect(); + + &cache.pages.iter().for_each(|(path, page)| { + nodes.push([("id".to_string(), path.clone())].iter().cloned().collect()); + page.links + .iter() + .filter(|link| page_ids.contains(link)) + .for_each(|link| { + links.push( + [ + ("source".to_string(), path.clone()), + ("target".to_string(), link.clone()), + ] + .iter() + .cloned() + .collect(), + ) + }) + }); + + context.insert("nodes", &nodes); + context.insert("links", &links); + + graph_page.title = "Graph View".to_string(); + graph_page.html = data + .tera + .render("graph.html", &context) + .map_err(ErrorInternalServerError)?; + Some(&graph_page) + }; // Recently changed let mut recently_changed = cache diff --git a/templates/graph.html b/templates/graph.html new file mode 100644 index 0000000..067a165 --- /dev/null +++ b/templates/graph.html @@ -0,0 +1,21 @@ +
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/templates/graph.js b/templates/graph.js new file mode 100644 index 0000000..e27b4e7 --- /dev/null +++ b/templates/graph.js @@ -0,0 +1,87 @@ +// const nodes = [ +// {"id": "Alice"}, +// {"id": "Bob"}, +// {"id": "Carol"} +// ]; +// +// const links = [ +// {"source": 0, "target": 1}, // Alice → Bob +// {"source": 1, "target": 0} // Bob → Carol +// ]; + +const graphEl = document.getElementById("graph"); + +const drag = (simulation) => { + function dragstarted(event, d) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + function dragended(event, d) { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + return d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended); +} + + +const simulation = d3.forceSimulation(nodes) + .force("link", d3.forceLink(links).id(d => d.id)) + .force("charge", d3.forceManyBody()) + .force("x", d3.forceX()) + .force("y", d3.forceY()); + +setTimeout(() => { + const width = graphEl.clientWidth; + const height = graphEl.clientHeight; + + const svg = d3.create("svg") + .attr("viewBox", [-width / 2, -height / 2, width, height]); + + const link = svg.append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line") + .attr("stroke-width", d => Math.sqrt(d.value)); + + const node = svg.append("g") + .attr("stroke", "#fff") + .attr("stroke-width", 1.5) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("r", 5) + .attr("fill", "red") + .call(drag(simulation)); + + node.append("title") + .text(d => d.id); + + simulation.on("tick", () => { + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + + node + .attr("cx", d => d.x) + .attr("cy", d => d.y); + }); + + + graphEl.appendChild(svg.node()); +}, 0); diff --git a/templates/main.css b/templates/main.css index 4ba664a..d4f080f 100644 --- a/templates/main.css +++ b/templates/main.css @@ -162,6 +162,10 @@ nav .timestamp:after { content: "]"; } +nav .graph-view { + text-align: center; +} + footer { padding: 1em 0; color: gray; diff --git a/templates/main.html b/templates/main.html index f8277e0..982a902 100644 --- a/templates/main.html +++ b/templates/main.html @@ -19,6 +19,7 @@ + @@ -72,6 +73,9 @@ {% endfor %} +
+ Graph view (beta) +