Skip to content

Commit

Permalink
Move css and js to appropriate files
Browse files Browse the repository at this point in the history
Having the CSS and JS in the html template produces pages larger
than necessary, as each page need to contain all the js/css.
Separating them in appropriate files allow the browser to just download
them once and use them for all the pages. This is even more effective
with an aggressive cache policy for the js and css, something that can
be done without fear thanks to the implemented cache-busting.
Also, having then in separate files allows us to use Hugo pipelines
for minimizing the code.
  • Loading branch information
claudio4 committed Mar 4, 2022
1 parent 5234fae commit 1313bd9
Show file tree
Hide file tree
Showing 12 changed files with 529 additions and 522 deletions.
File renamed without changes.
221 changes: 221 additions & 0 deletions assets/js/graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
async function drawGraph(url, baseUrl, pathColors, depth, enableDrag, enableLegend, enableZoom) {
const { index, links, content } = await fetchData()
const curPage = url.replace(baseUrl, "")

const parseIdsFromLinks = (links) => [...(new Set(links.flatMap(link => ([link.source, link.target]))))]

const neighbours = new Set()
const wl = [curPage || "/", "__SENTINEL"]
if (depth >= 0) {
while (depth >= 0 && wl.length > 0) {
// compute neighbours
const cur = wl.shift()
if (cur === "__SENTINEL") {
depth--
wl.push("__SENTINEL")
} else {
neighbours.add(cur)
const outgoing = index.links[cur] || []
const incoming = index.backlinks[cur] || []
wl.push(...outgoing.map(l => l.target), ...incoming.map(l => l.source))
}
}
} else {
parseIdsFromLinks(links).forEach(id => neighbours.add(id))
}

const data = {
nodes: [...neighbours].map(id => ({id})),
links: links.filter(l => neighbours.has(l.source) && neighbours.has(l.target)),
}

const color = (d) => {
if (d.id === curPage || (d.id === "/" && curPage === "")) {
return "var(--g-node-active)"
}

for (const pathColor of pathColors) {
const path = Object.keys(pathColor)[0]
const colour = pathColor[path]
if (d.id.startsWith(path)) {
return colour
}
}

return "var(--g-node)"
}

const drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(1).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;
}

const noop = () => {}
return d3.drag()
.on("start", enableDrag ? dragstarted : noop)
.on("drag", enableDrag ? dragged : noop)
.on("end", enableDrag ? dragended : noop);
}

const height = 250
const width = document.getElementById("graph-container").offsetWidth

const simulation = d3.forceSimulation(data.nodes)
.force("charge", d3.forceManyBody().strength(-30))
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("center", d3.forceCenter());

const svg = d3.select('#graph-container')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr("viewBox", [-width / 2, -height / 2, width, height]);

if (enableLegend) {
const legend = [
{"Current": "var(--g-node-active)"},
{"Note": "var(--g-node)"},
...pathColors
]
legend.forEach((legendEntry, i) => {
const key = Object.keys(legendEntry)[0]
const colour = legendEntry[key]
svg.append("circle").attr("cx", -width/2 + 20).attr("cy", height/2 - 30 * (i+1)).attr("r", 6).style("fill", colour)
svg.append("text").attr("x", -width/2 + 40).attr("y", height/2 - 30 * (i+1)).text(key).style("font-size", "15px").attr("alignment-baseline","middle")
})
}

// draw links between nodes
const link = svg.append("g")
.selectAll("line")
.data(data.links)
.join("line")
.attr("class", "link")
.attr("stroke", "var(--g-link)")
.attr("stroke-width", 2)
.attr("data-source", d => d.source.id)
.attr("data-target", d => d.target.id)

// svg groups
const graphNode = svg.append("g")
.selectAll("g")
.data(data.nodes)
.enter().append("g")

// draw individual nodes
const node = graphNode.append("circle")
.attr("class", "node")
.attr("id", (d) => d.id)
.attr("r", (d) => {
const numOut = index.links[d.id]?.length || 0
const numIn = index.backlinks[d.id]?.length || 0
return 3 + (numOut + numIn) / 4
})
.attr("fill", color)
.style("cursor", "pointer")
.on("click", (_, d) => {
window.location.href = baseUrl + '/' + decodeURI(d.id).replace(/\s+/g, '-')
})
.on("mouseover", function (_, d) {
d3.selectAll(".node")
.transition()
.duration(100)
.attr("fill", "var(--g-node-inactive)")

const neighbours = parseIdsFromLinks([...(index.links[d.id] || []), ...(index.backlinks[d.id] || [])])
const neighbourNodes = d3.selectAll(".node").filter(d => neighbours.includes(d.id))
const currentId = d.id
const linkNodes = d3.selectAll(".link").filter(d => d.source.id === currentId || d.target.id === currentId)

// highlight neighbour nodes
neighbourNodes
.transition()
.duration(200)
.attr("fill", color)

// highlight links
linkNodes
.transition()
.duration(200)
.attr("stroke", "var(--g-link-active)")

// show text for self
d3.select(this.parentNode)
.select("text")
.raise()
.transition()
.duration(200)
.style("opacity", 1)
}).on("mouseleave", function (_,d) {
d3.selectAll(".node")
.transition()
.duration(200)
.attr("fill", color)

const currentId = d.id
const linkNodes = d3.selectAll(".link").filter(d => d.source.id === currentId || d.target.id === currentId)

linkNodes
.transition()
.duration(200)
.attr("stroke", "var(--g-link)")

d3.select(this.parentNode)
.select("text")
.transition()
.duration(200)
.style("opacity", 0)
})
.call(drag(simulation));

// draw labels
const labels = graphNode.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text((d) => content[decodeURI(d.id).replace(/\s+/g, '-')]?.title || "Untitled")
.style("opacity", 0)
.style("pointer-events", "none")
.call(drag(simulation));

// set panning

if (enableZoom) {
svg.call(d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([0.25, 4])
.on("zoom", ({transform}) => {
link.attr("transform", transform);
node.attr("transform", transform);
labels.attr("transform", transform);
}));
}

// progress the simulation
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)
labels
.attr("x", d => d.x)
.attr("y", d => d.y)
});
}

34 changes: 34 additions & 0 deletions assets/js/popover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
function htmlToElement(html) {
const template = document.createElement('template')
html = html.trim()
template.innerHTML = html
return template.content.firstChild
}

function initPopover(base) {
const baseUrl = base.replace(window.location.origin, "") // is this useless?
document.addEventListener("DOMContentLoaded", () => {
fetchData().then(({content}) => {
const links = [...document.getElementsByClassName("internal-link")]
links.forEach(li => {
const linkDest = content[li.dataset.src.replace(baseUrl, "")]
// const linkDest = content[li.dataset.src]
if (linkDest) {
const popoverElement = `<div class="popover">
<h3>${linkDest.title}</h3>
<p>${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...</p>
<p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p>
</div>`
const el = htmlToElement(popoverElement)
li.appendChild(el)
li.addEventListener("mouseover", () => {
el.classList.add("visible")
})
li.addEventListener("mouseout", () => {
el.classList.remove("visible")
})
}
})
})
})
}
Loading

0 comments on commit 1313bd9

Please sign in to comment.