export default class GraphNav {
constructor() { // this.graphType set in initGraphType(); this.svgWrapper = document.getElementById('svg-graph'); this.graphTypeCheckBox = document.getElementById('graph-type-checkbox'); this.graphTypeEmojiSpan = document.getElementById('graph-type-emoji-span'); this.init(); } init() { this.initGraphType(); this.bindEvents(); this.drawD3Nav(); } bindEvents() { // listen for draw event (esp. from theme colors) this.svgWrapper.addEventListener('draw', () => { this.updateGraphType(); this.drawD3Nav(); }); this.graphTypeCheckBox.addEventListener('click', () => { this.updateGraphType(); this.drawD3Nav(); }); } // how to checkbox: https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_display_checkbox_text drawD3Nav() { // destroy old chart d3.select(this.svgWrapper).selectAll('svg > *').remove(); let theme_attrs = {}; // set theme-dependent graph attributes. if (document.getElementById('theme-colors-checkbox').checked) { theme_attrs = { "name": "dark", "radius": 2.5, "missing-radius": 2.5, } } else { theme_attrs = { "name": "light", "radius": 3, "missing-radius": 1.5, } } // redraw new chart if (this.graphTypeCheckBox.checked) { this.drawTree(theme_attrs); } else { this.drawNetWeb(theme_attrs); } } initGraphType() { this.graphType = localStorage.getItem('graph-type'); if (this.graphType !== "tree" && this.graphType !== "net-web") { this.graphType = '{{ site.graph_type }}'; } this.graphTypeCheckBox.checked = (this.graphType === "tree"); this.updateGraphType(); } updateGraphType() { if (this.graphTypeCheckBox.checked) { this.graphTypeEmojiSpan.innerText = "🕸"; this.graphType = "tree"; } else { this.graphTypeEmojiSpan.innerText = "🌳"; this.graphType = "net-web"; } localStorage.setItem('graph-type', this.graphType); } // d3 drawNetWeb (theme_attrs) { // d3.json has been async'd: https://stackoverflow.com/questions/49768165/code-within-d3-json-callback-is-not-executed d3.json("{{ site.baseurl }}/assets/graph-net-web.json") .then(function(data) { // console.log('d3 is building a tree'); // console.log(data); const svgWrapper = document.getElementById('svg-graph'); const width = +svgWrapper.getBoundingClientRect().width / 2; const height = +svgWrapper.getBoundingClientRect().height / 2; const svg = d3.select(svgWrapper) .attr("viewBox", [-width / 2, -height / 2, width, height]); const simulation = d3.forceSimulation() .nodes(data.nodes) .force("link", d3.forceLink() .id(function(d) {return d.id;}) .distance(30) .iterations(1) .links(data.links)) .force("charge", d3.forceManyBody().strength(-50)) .force("collide", d3.forceCollide()) .force("center", d3.forceCenter()) // see: https://stackoverflow.com/questions/9573178/d3-force-directed-layout-with-bounding-box?answertab=votes#tab-top // 'center of gravity' .force("forceX", d3.forceX() .strength(.3) .x(.75)) .force("forceY", d3.forceY() .strength(.1) .y(.9)); const link = svg.append("g") .attr("class", "links") .selectAll("line") .data(data.links) .enter().append("line"); const node = svg.append('g') .attr('class', 'nodes') .selectAll('g') .data(data.nodes) .join("g"); // .attr("active", (d) => isCurrentEntryInNetWeb(d) ? true : null) node.append('circle') //svg 2.0 not well-supported: https://stackoverflow.com/questions/47381187/svg-not-working-in-firefox // add attributes in javascript instead of css. .attr("r", (d) => isMissingEntryInNetWeb(d) ? theme_attrs["missing-radius"] : theme_attrs["radius"]) .attr("class", nodeTypeInNetWeb) .on("click", goToEntryFromNetWeb) .on("mouseover", onMouseover) .on("mouseout", onMouseout) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) .touchable(true)); // from: https://stackoverflow.com/questions/28415005/d3-js-selection-conditional-rendering // use filtering to deal with specific nodes // from: https://codepen.io/blackjacques/pen/BaaqKpO // add node pulse on the current node node.filter( function(d,i) { return isCurrentEntryInNetWeb(d); }) .append("circle") .attr("r", theme_attrs["radius"]) .classed("pulse", (d) => isCurrentEntryInNetWeb(d) ? true : null) .on("mouseover", onMouseover) .on("mouseout", onMouseout) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) .touchable(true)); node.filter( function(d,i) { return isPostTaggedInNetWeb(d); }) .append("circle") .attr("r", theme_attrs["radius"]) .classed("pulse-sem-tag", (d) => isPostTaggedInNetWeb(d) ? true : null) .on("mouseover", onMouseover) .on("mouseout", onMouseout) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) .touchable(true)); const text = svg.append('g') .attr('class', 'text') .selectAll('text') .data(data.nodes) .join("text") .attr("font-size", "20%") .attr("dx", 5) .attr("dy", ".05em") .text((d) => isMissingEntryInNetWeb(d) ? "Missing Entry" : d.label) .on("mouseover", onMouseover) .on("mouseout", onMouseout); simulation.on("tick", () => { link .attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x; }) .attr("y2", function(d) { return d.target.y; }); node .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); text .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); }); // // helpers // function isCurrentEntryInNetWeb(node) { return !isMissingEntryInNetWeb(node) && window.location.pathname.includes(node.url); } function isPostTaggedInNetWeb(node) { // const isPostPage = window.location.pathname.includes("post"); // if (!isPostPage) return false; const semTags = Array.from(document.getElementsByClassName("sem-tag")); const tagged = semTags.filter((semTag) => !isMissingEntryInNetWeb(node) && semTag.href.includes(node.url) ); return tagged.length !== 0; } function nodeTypeInNetWeb(node) { const isVisited = isVisitedEntryInNetWeb(node); const isMissing = isMissingEntryInNetWeb(node); if (isVisited) { return "visited"; } else if (!isVisited && !isMissing) { return "unvisited"; } else if (isMissing) { return "missing"; } else { console.log("WARN: Not a valid node type."); return null; } } function isVisitedEntryInNetWeb(node) { if (!isMissingEntryInNetWeb(node)) { var visited = JSON.parse(localStorage.getItem('visited')); for (let i = 0; i < visited.length; i++) { if (visited[i]['url'] === node.url) return true; } } return false; } function isMissingEntryInNetWeb(node) { return node.url === ''; } // from: https://stackoverflow.com/questions/63693132/unable-to-get-node-datum-on-mouseover-in-d3-v6 // d6 now passes events in vanilla javascript fashion function goToEntryFromNetWeb (e, d) { if (!isMissingEntryInNetWeb(d)) { window.location.href = d.url; } else { return null; } }; function onMouseover(e, d) { const linkedNodesSet = new Set(); data.links .filter((n) => n.target.id == d.id || n.source.id == d.id) .forEach((n) => { linkedNodesSet.add(n.target.id); linkedNodesSet.add(n.source.id); }); node.attr("class", (node_d) => { if (node_d.id !== d.id && !linkedNodesSet.has(node_d.id)) { return "inactive"; } return "active"; }); link.attr("class", (link_d) => { if (link_d.source.id !== d.id && link_d.target.id !== d.id) { return "inactive"; } return "active"; }); text.attr("class", (text_d) => { if (text_d.id !== d.id) { return "inactive"; } return "active"; }); }; function onMouseout(d) { node.attr("class", ""); link.attr("class", ""); text.attr("class", ""); }; function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).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; } }) .catch(function(error) { console.log(error); }); } drawTree (theme_attrs) { // d3.json has been async'd: https://stackoverflow.com/questions/49768165/code-within-d3-json-callback-is-not-executed d3.json("{{ site.baseurl }}/assets/graph-tree.json") .then(function(data) { // console.log('d3 is building a tree'); // console.log(data); const svgWrapper = document.getElementById('svg-graph'); const width = +svgWrapper.getBoundingClientRect().width / 2; const height = +svgWrapper.getBoundingClientRect().height / 2; const svg = d3.select(svgWrapper) .attr("viewBox", [-width / 2, -height / 2, width, height]); const root = d3.hierarchy(data); const links = root.links(); flatten(root); const nodes = root.descendants(); const simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id).distance(0).strength(1)) .force("charge", d3.forceManyBody().strength(-50)) .force("collide", d3.forceCollide()) .force("center", d3.forceCenter()) // see: https://stackoverflow.com/questions/9573178/d3-force-directed-layout-with-bounding-box?answertab=votes#tab-top // 'center of gravity' .force("forceX", d3.forceX() .strength(.3) .x(.9)) .force("forceY", d3.forceY() .strength(.1) .y(.9)); const link = svg.append("g") .attr("class", "links") .selectAll("line") .data(links) .join("line"); const node = svg.append('g') .attr('class', 'nodes') .selectAll('g') .data(nodes) .join("g"); node.append('circle') //svg 2.0 not well-supported: https://stackoverflow.com/questions/47381187/svg-not-working-in-firefox // add attributes in javascript instead of css. .attr("r", (d) => isMissingEntryInTree(d.data.id) ? theme_attrs["missing-radius"] : theme_attrs["radius"]) .attr("class", nodeTypeInTree) .on("click", goToEntryFromTree) .on("mouseover", onMouseover) .on("mouseout", onMouseout) // 🐛 bug: this does not work -- it overtakes clicks (extra lines in "tick" are related). .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) .touchable(true)); // from: https://stackoverflow.com/questions/28415005/d3-js-selection-conditional-rendering // use filtering to deal with specific nodes // from: https://codepen.io/blackjacques/pen/BaaqKpO // add node pulse on the current node node.filter( function(d,i) { return isCurrentEntryInTree(d); }) .append("circle") .attr("r", (d) => theme_attrs["radius"]) .classed("pulse", true) .on("mouseover", onMouseover) .on("mouseout", onMouseout) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) .touchable(true)); node.filter( function(d,i) { return isPostTaggedInTree(d); }) .append("circle") .attr("r", theme_attrs["radius"]) .classed("pulse-sem-tag", (d) => isPostTaggedInTree(d) ? true : null) .on("click", goToEntryFromTree) .on("mouseover", onMouseover) .on("mouseout", onMouseout) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) .touchable(true)); const text = svg.append('g') .attr('class', 'text') .selectAll('text') .data(nodes) .join("text") .attr("font-size", "20%") .attr("dx", 5) .attr("dy", ".05em") .text((d) => isMissingEntryInTree(d.data.id) ? "Missing Entry" : d.data.label) .on("mouseover", onMouseover) .on("mouseout", onMouseout); simulation.on("tick", () => { // from: https://mbostock.github.io/d3/talk/20110921/parent-foci.html // preserve hierarchical shape via link positioning var kx = .2 * simulation.alpha(); var ky = 1.3 * simulation.alpha(); links.forEach(function(d, i) { d.target.x += (d.source.x - d.target.x) * kx; d.target.y += (d.source.y + (height * .35) - d.target.y) * ky; }); 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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); text .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); }); // // helpers // function isCurrentEntryInTree(node) { return !isMissingEntryInTree(node.data.id) && window.location.pathname.includes(node.data.url); } function isPostTaggedInTree(node) { // const isPostPage = window.location.pathname.includes("post"); // if (!isPostPage) return false; const semTags = Array.from(document.getElementsByClassName("sem-tag")); const tagged = semTags.filter((semTag) => !isMissingEntryInTree(node.data.id) && semTag.href.includes(node.data.url) ); return tagged.length !== 0; } function nodeTypeInTree(node) { const isVisited = isVisitedEntryInTree(node); const isMissing = isMissingEntryInTree(node.data.id); if (isVisited) { return "visited"; } else if (!isVisited && !isMissing) { return "unvisited"; } else if (isMissing) { return "missing"; } else { console.log("WARN: Not a valid node type."); return null; } } function isVisitedEntryInTree(node) { var visited = JSON.parse(localStorage.getItem('visited')); for (let i = 0; i < visited.length; i++) { if (visited[i]['url'] === node.data.url) { return true; } } return false; } function isMissingEntryInTree(nodeId) { return nodeId === ""; } // from: https://stackoverflow.com/questions/63693132/unable-to-get-node-datum-on-mouseover-in-d3-v6 // d6 now passes events in vanilla javascript fashion function goToEntryFromTree(e, d) { if (!isMissingEntryInTree(d.data.id)) { window.location.href = d.data.url; return true; } else { return false; } }; function onMouseover(e, d) { const linkedNodesSet = new Set(); links .filter((n) => n.target.data.id == d.data.id || n.source.data.id == d.data.id) .forEach((n) => { linkedNodesSet.add(n.target.data.id); linkedNodesSet.add(n.source.data.id); }); node.attr("class", (node_d) => { if (node_d.data.id !== d.data.id && !linkedNodesSet.has(node_d.data.id)) { return "inactive"; } return "active"; }); link.attr("class", (link_d) => { if (link_d.source.data.id !== d.data.id && link_d.target.data.id !== d.data.id) { return "inactive"; } return "active"; }); text.attr("class", (text_d) => { if (text_d.data.id !== d.data.id) { return "inactive"; } return "active"; }); }; function onMouseout(d) { node.attr("class", ""); link.attr("class", ""); text.attr("class", ""); }; function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).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; } function flatten(root) { var nodes = []; function recurse(node) { if (node.descendents) node.descendents.forEach(recurse); nodes.push(node); } recurse(root); return nodes; } }) .catch(function(error) { console.log(error); }); }
}