add rudimentary d3.js graph view
This commit is contained in:
parent
68279afeeb
commit
7634899fbb
5 changed files with 162 additions and 1 deletions
47
src/main.rs
47
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<HashMap<String, String>> = vec![];
|
||||
let mut links: Vec<HashMap<String, String>> = 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
|
||||
|
|
21
templates/graph.html
Normal file
21
templates/graph.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div id="graph">
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3-dispatch@3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3-quadtree@3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3-timer@3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3-force@3"></script>
|
||||
<script>
|
||||
const nodes = {{nodes | json_encode() | safe}};
|
||||
const links = {{links | json_encode() | safe}};
|
||||
</script>
|
||||
<script src="/static/graph.js"></script>
|
||||
|
||||
<style>
|
||||
#graph {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
87
templates/graph.js
Normal file
87
templates/graph.js
Normal file
|
@ -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);
|
|
@ -162,6 +162,10 @@ nav .timestamp:after {
|
|||
content: "]";
|
||||
}
|
||||
|
||||
nav .graph-view {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 1em 0;
|
||||
color: gray;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<meta property="og:image" content="/static/favicon.png" />
|
||||
<link rel="shortcut icon" href="/static/favicon.png" type="image/x-icon">
|
||||
<link href="/static/main.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -72,6 +73,9 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="graph-view">
|
||||
<a href="/!graph">Graph view (beta)</a>
|
||||
</section>
|
||||
</nav>
|
||||
<footer><a href="https://gitlab.com/tmladek/gardenserver">gardenserver {{version}}</a></footer>
|
||||
</aside>
|
||||
|
|
Loading…
Reference in a new issue