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
|
// Recently changed
|
||||||
let mut recently_changed = cache
|
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: "]";
|
content: "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav .graph-view {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
color: gray;
|
color: gray;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<meta property="og:image" content="/static/favicon.png" />
|
<meta property="og:image" content="/static/favicon.png" />
|
||||||
<link rel="shortcut icon" href="/static/favicon.png" type="image/x-icon">
|
<link rel="shortcut icon" href="/static/favicon.png" type="image/x-icon">
|
||||||
<link href="/static/main.css" rel="stylesheet">
|
<link href="/static/main.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="main.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -72,6 +73,9 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="graph-view">
|
||||||
|
<a href="/!graph">Graph view (beta)</a>
|
||||||
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
<footer><a href="https://gitlab.com/tmladek/gardenserver">gardenserver {{version}}</a></footer>
|
<footer><a href="https://gitlab.com/tmladek/gardenserver">gardenserver {{version}}</a></footer>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
Loading…
Reference in a new issue