403 lines
13 KiB
HTML
403 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>Hypertext 🌱 Digital Garden</title>
|
|||
|
<meta name="description" content="<p>Hypertext Markup Language</p>
|
|||
|
">
|
|||
|
<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>Hypertext</h1>
|
|||
|
<time datetime="2021-03-11T03:37:19+00:00">
|
|||
|
Last updated on March 11, 2021
|
|||
|
|
|||
|
</time>
|
|||
|
</div>
|
|||
|
|
|||
|
<div id="notes-entry-container">
|
|||
|
<content>
|
|||
|
<p>Hypertext Markup Language</p>
|
|||
|
|
|||
|
</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>
|
|||
|
|
|||
|
<script src="/assets/js/scripts.js" async></script>
|
|||
|
</body>
|
|||
|
</html>
|