add rudimentary d3.js graph view

master
Tomáš Mládek 2021-07-25 17:08:34 +02:00
parent 68279afeeb
commit 7634899fbb
5 changed files with 162 additions and 1 deletions

View File

@ -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
View 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
View 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);

View File

@ -162,6 +162,10 @@ nav .timestamp:after {
content: "]";
}
nav .graph-view {
text-align: center;
}
footer {
padding: 1em 0;
color: gray;

View File

@ -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>