-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- the plugin that calculates back-links between pages now uses the same information to create `pages_graph.json` which is then viewable as "Graph View" menu option at /graph
- Loading branch information
Showing
4 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,308 @@ | ||
<!-- This file is from https://github.com/maximevaillancourt/digital-garden-jekyll-template --> | ||
<style> | ||
.links line { | ||
stroke: #ccc; | ||
opacity: 0.5; | ||
} | ||
|
||
.nodes circle { | ||
cursor: pointer; | ||
fill: #8b88e6; | ||
transition: all 0.15s ease-out; | ||
} | ||
|
||
.text text { | ||
cursor: pointer; | ||
fill: #333; | ||
text-shadow: -1px -1px 0 #fafafabb, 1px -1px 0 #fafafabb, -1px 1px 0 #fafafabb, 1px 1px 0 #fafafabb; | ||
} | ||
|
||
.nodes [active], | ||
.text [active] { | ||
cursor: pointer; | ||
fill: black; | ||
} | ||
|
||
.inactive { | ||
opacity: 0.1; | ||
transition: all 0.15s ease-out; | ||
} | ||
|
||
#graph-wrapper { | ||
background: #fcfcfc; | ||
border-radius: 4px; | ||
height: auto; | ||
} | ||
</style> | ||
|
||
<div id="graph-wrapper"> | ||
<script> | ||
window.addEventListener("load", loadGraph); | ||
|
||
function loadGraph() { | ||
var oScript = document.createElement("script"); | ||
oScript.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"; | ||
oScript.crossOrigin = 'anonymous'; | ||
oScript.integrity = | ||
"sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg=="; | ||
document.body.appendChild(oScript); | ||
oScript.onload = () => { | ||
const MINIMAL_NODE_SIZE = 8; | ||
const MAX_NODE_SIZE = 12; | ||
const ACTIVE_RADIUS_FACTOR = 1.5; | ||
const STROKE = 1; | ||
const FONT_SIZE = 16; | ||
const TICKS = 200; | ||
const FONT_BASELINE = 40; | ||
const MAX_LABEL_LENGTH = 50; | ||
|
||
const graphData = {% include pages_graph.json %} | ||
let nodesData = graphData.nodes; | ||
let linksData = graphData.edges; | ||
|
||
const nodeSize = {}; | ||
|
||
const updateNodeSize = () => { | ||
nodesData.forEach((el) => { | ||
let weight = | ||
3 * | ||
Math.sqrt( | ||
linksData.filter((l) => l.source.id === el.id || l.target.id === el.id) | ||
.length + 1 | ||
); | ||
if (weight < MINIMAL_NODE_SIZE) { | ||
weight = MINIMAL_NODE_SIZE; | ||
} else if (weight > MAX_NODE_SIZE) { | ||
weight = MAX_NODE_SIZE; | ||
} | ||
nodeSize[el.id] = weight; | ||
}); | ||
}; | ||
|
||
const onClick = (d) => { | ||
window.location = d.path | ||
}; | ||
|
||
const onMouseover = function (d) { | ||
const relatedNodesSet = new Set(); | ||
linksData | ||
.filter((n) => n.target.id == d.id || n.source.id == d.id) | ||
.forEach((n) => { | ||
relatedNodesSet.add(n.target.id); | ||
relatedNodesSet.add(n.source.id); | ||
}); | ||
|
||
node.attr("class", (node_d) => { | ||
if (node_d.id !== d.id && !relatedNodesSet.has(node_d.id)) { | ||
return "inactive"; | ||
} | ||
return ""; | ||
}); | ||
|
||
link.attr("class", (link_d) => { | ||
if (link_d.source.id !== d.id && link_d.target.id !== d.id) { | ||
return "inactive"; | ||
} | ||
return ""; | ||
}); | ||
|
||
link.attr("stroke-width", (link_d) => { | ||
if (link_d.source.id === d.id || link_d.target.id === d.id) { | ||
return STROKE * 4; | ||
} | ||
return STROKE; | ||
}); | ||
text.attr("class", (text_d) => { | ||
if (text_d.id !== d.id && !relatedNodesSet.has(text_d.id)) { | ||
return "inactive"; | ||
} | ||
return ""; | ||
}); | ||
}; | ||
|
||
const onMouseout = function (d) { | ||
node.attr("class", ""); | ||
link.attr("class", ""); | ||
text.attr("class", ""); | ||
link.attr("stroke-width", STROKE); | ||
}; | ||
|
||
const sameNodes = (previous, next) => { | ||
if (next.length !== previous.length) { | ||
return false; | ||
} | ||
|
||
const map = new Map(); | ||
for (const node of previous) { | ||
map.set(node.id, node.label); | ||
} | ||
|
||
for (const node of next) { | ||
const found = map.get(node.id); | ||
if (!found || found !== node.title) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
const sameEdges = (previous, next) => { | ||
if (next.length !== previous.length) { | ||
return false; | ||
} | ||
|
||
const set = new Set(); | ||
for (const edge of previous) { | ||
set.add(`${edge.source.id}-${edge.target.id}`); | ||
} | ||
|
||
for (const edge of next) { | ||
if (!set.has(`${edge.source.id}-${edge.target.id}`)) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
const graphWrapper = document.getElementById('graph-wrapper') | ||
const element = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | ||
element.setAttribute("width", graphWrapper.getBoundingClientRect().width); | ||
element.setAttribute("height", window.innerHeight * 0.8); | ||
graphWrapper.appendChild(element); | ||
|
||
const reportWindowSize = () => { | ||
element.setAttribute("width", window.innerWidth); | ||
element.setAttribute("height", window.innerHeight); | ||
}; | ||
|
||
window.onresize = reportWindowSize; | ||
|
||
const svg = d3.select("svg"); | ||
const width = Number(svg.attr("width")); | ||
const height = Number(svg.attr("height")); | ||
let zoomLevel = 1; | ||
|
||
const simulation = d3 | ||
.forceSimulation(nodesData) | ||
.force("forceX", d3.forceX().x(width / 2)) | ||
.force("forceY", d3.forceY().y(height / 2)) | ||
.force("charge", d3.forceManyBody()) | ||
.force( | ||
"link", | ||
d3 | ||
.forceLink(linksData) | ||
.id((d) => d.id) | ||
.distance(70) | ||
) | ||
.force("center", d3.forceCenter(width / 2, height / 2)) | ||
.force("collision", d3.forceCollide().radius(80)) | ||
.stop(); | ||
|
||
const g = svg.append("g"); | ||
let link = g.append("g").attr("class", "links").selectAll(".link"); | ||
let node = g.append("g").attr("class", "nodes").selectAll(".node"); | ||
let text = g.append("g").attr("class", "text").selectAll(".text"); | ||
|
||
const resize = () => { | ||
if (d3.event) { | ||
const scale = d3.event.transform; | ||
zoomLevel = scale.k; | ||
g.attr("transform", scale); | ||
} | ||
|
||
const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value); | ||
|
||
const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1); | ||
|
||
text.attr("font-size", (d) => font); | ||
text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8); | ||
link.attr("stroke-width", zoomOrKeep(STROKE)); | ||
node.attr("r", (d) => { | ||
return zoomOrKeep(nodeSize[d.id]); | ||
}); | ||
svg | ||
.selectAll("circle") | ||
.filter((_d, i, nodes) => d3.select(nodes[i]).attr("active")) | ||
.attr("r", (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id])); | ||
}; | ||
|
||
const ticked = () => { | ||
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); | ||
text | ||
.attr("x", (d) => d.x) | ||
.attr("y", (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel); | ||
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); | ||
}; | ||
|
||
const restart = () => { | ||
updateNodeSize(); | ||
node = node.data(nodesData, (d) => d.id); | ||
node.exit().remove(); | ||
node = node | ||
.enter() | ||
.append("circle") | ||
.attr("r", (d) => { | ||
return nodeSize[d.id]; | ||
}) | ||
.on("click", onClick) | ||
.on("mouseover", onMouseover) | ||
.on("mouseout", onMouseout) | ||
.merge(node); | ||
|
||
link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`); | ||
link.exit().remove(); | ||
link = link.enter().append("line").attr("stroke-width", STROKE).merge(link); | ||
|
||
text = text.data(nodesData, (d) => d.label); | ||
text.exit().remove(); | ||
text = text | ||
.enter() | ||
.append("text") | ||
.text((d) => shorten(d.label.replace(/_*/g, ""), MAX_LABEL_LENGTH)) | ||
.attr("font-size", `${FONT_SIZE}px`) | ||
.attr("text-anchor", "middle") | ||
.attr("alignment-baseline", "central") | ||
.on("click", onClick) | ||
.on("mouseover", onMouseover) | ||
.on("mouseout", onMouseout) | ||
.merge(text); | ||
|
||
node.attr("active", (d) => isCurrentPath(d.path) ? true : null); | ||
text.attr("active", (d) => isCurrentPath(d.path) ? true : null); | ||
|
||
simulation.nodes(nodesData); | ||
simulation.force("link").links(linksData); | ||
simulation.alpha(1).restart(); | ||
simulation.stop(); | ||
|
||
for (let i = 0; i < TICKS; i++) { | ||
simulation.tick(); | ||
} | ||
|
||
ticked(); | ||
}; | ||
|
||
const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on("zoom", resize); | ||
|
||
zoomHandler(svg); | ||
restart(); | ||
|
||
function isCurrentPath(notePath) { | ||
return window.location.pathname.includes(notePath) | ||
} | ||
|
||
function shorten(str, maxLen, separator = ' ') { | ||
if (str.length <= maxLen) return str; | ||
return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...'; | ||
} | ||
} | ||
} | ||
</script> | ||
</div> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
layout: default | ||
title: Graph | ||
permalink: /graph/ | ||
--- | ||
|
||
|
||
<p>Graph visualization of all the pages and their links.</p> | ||
|
||
{% include pages_graph.html %} |