402 lines
13 KiB
HTML
402 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="en"><head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Protocols 🌱 Digital Garden</title>
|
||
<meta name="description" content="
|
||
">
|
||
<link rel="stylesheet" href="/assets/css/style.css"><link type="application/atom+xml" rel="alternate" href="/feed.xml" title="Digital Garden" /></head><body class="dark-green garamond links-dark-green bg-washed-green"><header class="f-6 flex"><a class="no-underline-hover" rel="author" href="/">Digital Garden</a><nav>, <a class="no-underline-hover" href="/about/">About</a></nav></header><main aria-label="Content">
|
||
<div class="flex">
|
||
<article class="w-50">
|
||
<div>
|
||
<h1>Protocols</h1>
|
||
<time datetime="2021-03-11T03:39:29+00:00">
|
||
Last updated on March 11, 2021
|
||
|
||
</time>
|
||
</div>
|
||
|
||
<div id="notes-entry-container">
|
||
<content>
|
||
|
||
|
||
</content>
|
||
|
||
<side style="font-size: 0.9em">
|
||
<h3 style="margin-bottom: 1em">Notes mentioning this note</h3>
|
||
|
||
<div style="display: grid; grid-gap: 1em; grid-template-columns: repeat(1fr);">
|
||
|
||
<div class="backlink-box">
|
||
<a class="internal-link" href="/statement-of-intent">Statement of Intent</a><br>
|
||
<div style="font-size: 0.9em">Hypha’s practice is situated across many topics that are present in the theme of Adaptive Reuse & Creative Misuse. Drawing...</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</side>
|
||
</div>
|
||
</article>
|
||
|
||
<div class="w-50">
|
||
|
||
<p>Here are all the notes in this garden, along with their links, visualized as a graph.</p>
|
||
|
||
<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="zoom"></div>
|
||
<div id="graph-wrapper">
|
||
<script>
|
||
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 = {"edges":[{"source":"229320080329284392969231993","target":"50678562812897"},{"source":"229320080329284392969231993","target":"72697617314704"},{"source":"229320080329284392969231993","target":"36028"}],"nodes":[{"id":"50678562812897","path":"/hypertext","label":"Hypertext"},{"id":"72697617314704","path":"/protocols","label":"Protocols"},{"id":"36028","path":"/rss","label":"RSS"},{"id":"229320080329284392969231993","path":"/statement-of-intent","label":"Statement of Intent"}]}
|
||
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 === el.id || l.target === 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}-${edge.target}`)) {
|
||
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]));
|
||
|
||
document.getElementById("zoom").innerHTML = zoomLevel.toFixed(2);
|
||
};
|
||
|
||
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>
|
||
</div>
|
||
</div>
|
||
</main><footer>
|
||
|
||
</footer><!-- That file is not particularly elegant. This will need a refactor at some point. -->
|
||
<div style="opacity: 0; display: none;" id='tooltip-wrapper'>
|
||
<div id='tooltip-content'>
|
||
</div>
|
||
</div>
|
||
|
||
<iframe style="display: none; height: 0; width: 0;" id='link-preview-iframe' src="">
|
||
</iframe>
|
||
|
||
<script>
|
||
var opacityTimeout;
|
||
var contentTimeout;
|
||
var transitionDurationMs = 100;
|
||
|
||
var iframe = document.getElementById('link-preview-iframe')
|
||
var tooltipWrapper = document.getElementById('tooltip-wrapper')
|
||
var tooltipContent = document.getElementById('tooltip-content')
|
||
|
||
function hideTooltip() {
|
||
opacityTimeout = setTimeout(function() {
|
||
tooltipWrapper.style.opacity = 0;
|
||
contentTimeout = setTimeout(function() {
|
||
tooltipContent.innerHTML = '';
|
||
tooltipWrapper.style.display = 'none';
|
||
}, transitionDurationMs + 1);
|
||
}, transitionDurationMs)
|
||
}
|
||
|
||
function showTooltip(event) {
|
||
var elem = event.target;
|
||
var elem_props = elem.getClientRects()[elem.getClientRects().length - 1];
|
||
var top = window.pageYOffset || document.documentElement.scrollTop
|
||
|
||
if (event.target.host === window.location.host) {
|
||
iframe.src = event.target.href
|
||
iframe.onload = function() {
|
||
tooltipContentHtml = ''
|
||
tooltipContentHtml += '<div style="font-weight: bold;">' + iframe.contentWindow.document.querySelector('h1').innerHTML + '</div>'
|
||
tooltipContentHtml += iframe.contentWindow.document.querySelector('content').innerHTML
|
||
|
||
tooltipContent.innerHTML = tooltipContentHtml
|
||
|
||
tooltipWrapper.style.display = 'block';
|
||
setTimeout(function() {
|
||
tooltipWrapper.style.opacity = 1;
|
||
}, 1)
|
||
}
|
||
|
||
tooltipWrapper.style.left = elem_props.left - (tooltipWrapper.offsetWidth / 2) + (elem_props.width / 2) + "px";
|
||
if ((window.innerHeight - elem_props.top) < (tooltipWrapper.offsetHeight)) {
|
||
tooltipWrapper.style.top = elem_props.top + top - tooltipWrapper.offsetHeight - 10 + "px";
|
||
} else if ((window.innerHeight - elem_props.top) > (tooltipWrapper.offsetHeight)) {
|
||
tooltipWrapper.style.top = elem_props.top + top + 35 + "px";
|
||
}
|
||
|
||
if ((elem_props.left + (elem_props.width / 2)) < (tooltipWrapper.offsetWidth / 2)) {
|
||
tooltipWrapper.style.left = "10px";
|
||
} else if ((document.body.clientWidth - elem_props.left - (elem_props.width / 2)) < (tooltipWrapper.offsetWidth / 2)) {
|
||
tooltipWrapper.style.left = document.body.clientWidth - tooltipWrapper.offsetWidth - 20 + "px";
|
||
}
|
||
}
|
||
}
|
||
|
||
function setupListeners(linkElement) {
|
||
linkElement.addEventListener('mouseleave', function(_event) {
|
||
hideTooltip();
|
||
});
|
||
|
||
tooltipWrapper.addEventListener('mouseleave', function(_event) {
|
||
hideTooltip();
|
||
});
|
||
|
||
linkElement.addEventListener('mouseenter', function(event) {
|
||
clearTimeout(opacityTimeout);
|
||
clearTimeout(contentTimeout);
|
||
showTooltip(event);
|
||
});
|
||
|
||
tooltipWrapper.addEventListener('mouseenter', function(event) {
|
||
clearTimeout(opacityTimeout);
|
||
clearTimeout(contentTimeout);
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('content a').forEach(setupListeners);
|
||
</script>
|
||
|
||
</body>
|
||
</html> |