Add notes graph visualization
This commit is contained in:
parent
b6849b30aa
commit
e0005f58ba
196
_includes/notes_graph.html
Normal file
196
_includes/notes_graph.html
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<style>
|
||||||
|
.links line {
|
||||||
|
stroke: #ccc;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes circle {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: blue;
|
||||||
|
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: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes circle[active] {
|
||||||
|
r: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-wrapper {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"
|
||||||
|
integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg=="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<div id="graph-wrapper">
|
||||||
|
<script>
|
||||||
|
const RADIUS = 4;
|
||||||
|
const STROKE = 1;
|
||||||
|
const FONT_SIZE = 15;
|
||||||
|
const TICKS = 100;
|
||||||
|
const FONT_BASELINE = 15;
|
||||||
|
const MAX_LABEL_LENGTH = 50;
|
||||||
|
|
||||||
|
const graphData = {% include notes_graph.json %}
|
||||||
|
|
||||||
|
let nodesData = graphData.nodes;
|
||||||
|
let linksData = graphData.edges;
|
||||||
|
|
||||||
|
const onClick = (d) => {
|
||||||
|
window.location = d.path
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"svg"
|
||||||
|
);
|
||||||
|
const graphWrapper = document.getElementById('graph-wrapper')
|
||||||
|
|
||||||
|
element.setAttribute("width", graphWrapper.getBoundingClientRect().width);
|
||||||
|
graphWrapper.setAttribute("height", window.innerHeight * 0.8);
|
||||||
|
element.setAttribute("height", window.innerHeight * 0.8);
|
||||||
|
|
||||||
|
graphWrapper.appendChild(element);
|
||||||
|
|
||||||
|
const reportWindowSize = () => {
|
||||||
|
element.setAttribute("width", graphWrapper.getBoundingClientRect().width);
|
||||||
|
graphWrapper.setAttribute("height", window.innerHeight * 0.8);
|
||||||
|
element.setAttribute("height", window.innerHeight * 0.8);
|
||||||
|
};
|
||||||
|
|
||||||
|
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("charge", d3
|
||||||
|
.forceManyBody()
|
||||||
|
.strength(-4000)
|
||||||
|
)
|
||||||
|
.force(
|
||||||
|
"link",
|
||||||
|
d3
|
||||||
|
.forceLink(linksData)
|
||||||
|
.id((d) => d.id)
|
||||||
|
.distance(150)
|
||||||
|
)
|
||||||
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||||
|
.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 zoomActions = () => {
|
||||||
|
const scale = d3.event.transform;
|
||||||
|
zoomLevel = scale.k;
|
||||||
|
g.attr("transform", scale);
|
||||||
|
|
||||||
|
const zoomOrKeep = (value) => (scale.k >= 1 ? value / scale.k : value);
|
||||||
|
|
||||||
|
const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1);
|
||||||
|
|
||||||
|
text.attr("font-size", `${font}px`);
|
||||||
|
text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE));
|
||||||
|
link.attr("stroke-width", zoomOrKeep(STROKE));
|
||||||
|
node.attr("r", zoomOrKeep(RADIUS));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 / 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 = () => {
|
||||||
|
node = node.data(nodesData, (d) => d.id);
|
||||||
|
node.exit().remove();
|
||||||
|
node = node
|
||||||
|
.enter()
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", RADIUS)
|
||||||
|
// .attr("fill", (d) => getNodeColor(d))
|
||||||
|
.on("click", onClick)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
node.attr("active", (d) => isCurrentPath(d.path) ? true : null);
|
||||||
|
text.attr("active", (d) => isCurrentPath(d.path) ? true : null);
|
||||||
|
|
||||||
|
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)
|
||||||
|
.merge(text);
|
||||||
|
|
||||||
|
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])
|
||||||
|
//.translateExtent([[0,0], [width, height]])
|
||||||
|
//.extent([[0, 0], [width, height]])
|
||||||
|
.on("zoom", zoomActions);
|
||||||
|
|
||||||
|
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>
|
||||||
@ -39,3 +39,8 @@ layout: default
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>Here are all the notes in this garden, along with their links, visualized as a graph.</p>
|
||||||
|
|
||||||
|
{% include notes_graph.html %}
|
||||||
|
|||||||
@ -8,3 +8,7 @@ permalink: /
|
|||||||
# Welcome! 🌱
|
# Welcome! 🌱
|
||||||
|
|
||||||
This is your digital garden. Here's [[Your first seed]] to get started on your exploration.
|
This is your digital garden. Here's [[Your first seed]] to get started on your exploration.
|
||||||
|
|
||||||
|
<p>Here are all the notes in this garden, along with their links, visualized as a graph.</p>
|
||||||
|
|
||||||
|
{% include notes_graph.html %}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class BidirectionalLinksGenerator < Jekyll::Generator
|
class BidirectionalLinksGenerator < Jekyll::Generator
|
||||||
def generate(site)
|
def generate(site)
|
||||||
|
graph_nodes = []
|
||||||
|
graph_edges = []
|
||||||
|
|
||||||
all_notes = site.collections['notes'].docs
|
all_notes = site.collections['notes'].docs
|
||||||
all_pages = site.pages
|
all_pages = site.pages
|
||||||
|
|
||||||
@ -19,11 +22,37 @@ class BidirectionalLinksGenerator < Jekyll::Generator
|
|||||||
|
|
||||||
# Identify note backlinks and add them to each note
|
# Identify note backlinks and add them to each note
|
||||||
all_notes.each do |current_note|
|
all_notes.each do |current_note|
|
||||||
|
# Nodes: Jekyll
|
||||||
notes_linking_to_current_note = all_notes.filter do |e|
|
notes_linking_to_current_note = all_notes.filter do |e|
|
||||||
e.content.include?(current_note.url)
|
e.content.include?(current_note.url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Nodes: Graph
|
||||||
|
graph_nodes << {
|
||||||
|
id: note_id_from_note(current_note),
|
||||||
|
path: current_note.url,
|
||||||
|
label: current_note.title,
|
||||||
|
} unless current_note.path.include?('_notes/index.html')
|
||||||
|
|
||||||
|
# Edges: Jekyll
|
||||||
current_note.data['backlinks'] = notes_linking_to_current_note
|
current_note.data['backlinks'] = notes_linking_to_current_note
|
||||||
|
|
||||||
|
# Edges: Graph
|
||||||
|
notes_linking_to_current_note.each do |n|
|
||||||
|
graph_edges << {
|
||||||
|
source: note_id_from_note(n),
|
||||||
|
target: note_id_from_note(current_note),
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
File.write('_includes/notes_graph.json', JSON.dump({
|
||||||
|
edges: graph_edges,
|
||||||
|
nodes: graph_nodes,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def note_id_from_note(note)
|
||||||
|
note.title.to_i(36).to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user