Skip to content

Instantly share code, notes, and snippets.

@bhpayne
Created May 25, 2020 21:12
Show Gist options
  • Save bhpayne/c0fc326b974f19f10f6e616caaceb70e to your computer and use it in GitHub Desktop.
Save bhpayne/c0fc326b974f19f10f6e616caaceb70e to your computer and use it in GitHub Desktop.
d3js graph with hyperlinked images for nodes
{
"nodes": [
{"id": "9040079362", "group": 0, "img": "https://derivationmap.net/static/9040079362.png", "url": "https://derivationmap.net/list_all_expressions?referrer=d3js#9040079362", "width": 18, "height": 30, "linear index": 0},
{"id": "3131111133", "group": 0, "img": "https://derivationmap.net/static/3131111133.png", "url": "https://derivationmap.net/list_all_expressions?referrer=d3js#3131111133", "width": 121, "height": 34, "linear index": 0},
{"id": "4718871", "group": 2, "img": "https://derivationmap.net/static/multiplybothsidesby.png", "url": "https://derivationmap.net/list_all_inference_rules?referrer=d3js#multiply both sides by", "width": 309, "height": 32, "linear index": 2},
{"id": "2113211456", "group": 0, "img": "https://derivationmap.net/static/2113211456.png", "url": "https://derivationmap.net/list_all_expressions?referrer=d3js#2113211456", "width": 121, "height": 34, "linear index": 0},
{"id": "2131616531", "group": 0, "img": "https://derivationmap.net/static/2131616531.png", "url": "https://derivationmap.net/list_all_expressions?referrer=d3js#2131616531", "width": 103, "height": 30, "linear index": 0},
{"id": "2169431", "group": 3, "img": "https://derivationmap.net/static/dividebothsidesby.png", "url": "https://derivationmap.net/list_all_inference_rules?referrer=d3js#divide both sides by", "width": 276, "height": 31, "linear index": 3},
{"id": "9278347", "group": 1, "img": "https://derivationmap.net/static/declareinitialexpr.png", "url": "https://derivationmap.net/list_all_inference_rules?referrer=d3js#declare initial expr", "width": 257, "height": 32, "linear index": 1},
{"id": "6286448", "group": 4, "img": "https://derivationmap.net/static/declarefinalexpr.png", "url": "https://derivationmap.net/list_all_inference_rules?referrer=d3js#declare final expr", "width": 236, "height": 32, "linear index": 4},
{"id": "9565166889", "group": 0, "img": "https://derivationmap.net/static/9565166889.png", "url": "https://derivationmap.net/list_all_expressions?referrer=d3js#9565166889", "width": 24, "height": 23, "linear index": 0}
],
"links": [
{"source": "9565166889", "target": "2169431", "value": 1},
{"source": "2169431", "target": "2113211456", "value": 1},
{"source": "9278347", "target": "3131111133", "value": 1},
{"source": "9040079362", "target": "4718871", "value": 1},
{"source": "4718871", "target": "2131616531", "value": 1},
{"source": "2113211456", "target": "6286448", "value": 1},
{"source": "3131111133", "target": "4718871", "value": 1},
{"source": "2131616531", "target": "2169431", "value": 1}
]
}
<html>
<head>
<title>Welcome to the Physics Derivation Graph</title>
<!-- https://www.w3schools.com/html/html_css.asp -->
<style>
/* SVG region
https://chartio.com/resources/tutorials/how-to-resize-an-svg-when-the-window-is-resized-in-d3-js/
*/
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%;
vertical-align: top;
overflow: hidden;
}
.svg-content {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
/* end SVG region */
/* a bit of space around the edges looks cleaner */
html, body {
height: 100%;
margin: 5;
padding: 5;
}
</style>
</head>
<body>
<center>
<H2>Physics Derivation Graph</H2>
</center>
<P>
<center>
<svg id="viz"></svg>
</center>
</P>
<script src="https://d3js.org/d3.v5.min.js"></script>
<!-- code below is based on https://bl.ocks.org/mapio/53fed7d84cd1812d6a6639ed7aa83868
and is better than https://bl.ocks.org/mbostock/950642
because mapio's implementation features zoom and node coloring -->
<script>
var border = 1;
var bordercolor="black";
var color = d3.scaleOrdinal(d3.schemeCategory10); // coloring of nodes
//d3.json("https://derivationmap.net/static/884319.json").then(function(graph) {
d3.json("884319.json").then(function(graph) {
var node_count = graph["nodes"].length;
// the following does not automatically resize as the window changes size
// see https://bl.ocks.org/rudedogg187/8c86bae1f1eb0ac4008c5f7ff5f172a6
// and https://bl.ocks.org/curran/3a68b0c81991e2e94b19
if (node_count <= 10) {
// https://github.com/allofphysicsgraph/proofofconcept/issues/163
console.log("inside if")
var width = 600;
var height = 350;
} else {
console.log("inside else")
var width = window.innerWidth*0.9;
var height = window.innerHeight*0.8;
}
var label = {
"nodes": [],
"links": []
};
graph.nodes.forEach(function(d, i) {
label.nodes.push({node: d});
label.nodes.push({node: d});
label.links.push({
source: i * 2,
target: i * 2 + 1
});
});
var labelLayout = d3.forceSimulation(label.nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(label.links).distance(0).strength(2));
var graphLayout = d3.forceSimulation(graph.nodes)
.force("charge", d3.forceManyBody().strength(-3000))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(1))
.force("y", d3.forceY(height / 2).strength(1))
.force("link", d3.forceLink(graph.links).id(function(d) {return d.id; }).distance(50).strength(1))
.on("tick", ticked);
var adjlist = [];
graph.links.forEach(function(d) {
adjlist[d.source.index + "-" + d.target.index] = true;
adjlist[d.target.index + "-" + d.source.index] = true;
});
function neigh(a, b) {
return a == b || adjlist[a + "-" + b];
}
var svg = d3.select("#viz").attr("width", width).attr("height", height);
// https://chartio.com/resources/tutorials/how-to-resize-an-svg-when-the-window-is-resized-in-d3-js/
//var svg = d3.select("div#container")
// .append("svg")
// .attr("preserveAspectRatio", "xMinYMin meet")
// .attr("viewBox", "0 0 300 300")
// .attr("border",border)
// .classed("svg-content", true);
// http://bl.ocks.org/rkirsling/5001347
// define arrow markers for graph links
svg.append("svg:defs").append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -5 5 10") // min-x, min-y, width, height; https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
.attr("refX", 8) // x coordinate of an element's reference point; https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/refX
.attr("markerWidth", 20) // width of the viewport; https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/markerWidth
.attr("markerHeight", 20)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-3L6,0L0,3") // draw line starting at 0,-5; connect to 8,0, connect to 0,5
.attr("fill", "gray");
// http://bl.ocks.org/AndrewStaroscik/5222370
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
var container = svg.append("g");
svg.call(
d3.zoom()
.scaleExtent([.1, 4])
.on("zoom", function() { container.attr("transform", d3.event.transform); })
);
var link = container.append("g").attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke", "#aaa")
.attr("stroke-width", "2px")
.attr("marker-end","url(#end-arrow)");
var node = container.append("g").attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter() // https://www.d3indepth.com/enterexit/
// to hyperlink the node circle, uncomment the next three lines
// .append("a")
// .attr('href', function(d) { return d.url })
// .attr('target', '_blank')
.append("circle")
.attr("r", 10)
.attr("fill", function(d) { return color(d.group); });
// written by https://www.freelancer.com/u/Wawa27 for $20 on Freelancer
var labelNode = container.append("g").attr("class", "labelNodes")
.selectAll("text")
.data(label.nodes)
.enter() // https://www.d3indepth.com/enterexit/
// the following three lines hyperlink the image
.append("a")
.attr('href', function(d, i) { return d.node.url })
.attr('target', '_blank')
.append("image")
// alternative option, unverified: https://stackoverflow.com/questions/39908583/d3-js-labeling-nodes-with-image-in-force-layout
// BHP does not know why the i%2 is needed; without it the graph gets two images per node
// see "label.links.push" above which uses this even/odd identifier
// switching between i%2==1 and i%2==0 produces different image locations (?)
.attr("xlink:href", function(d, i) { return i % 2 == 1 ? "" : d.node.img; } )
.attr("x", 4)
.attr("y", 0)
// the following alter the image size
.attr("width", function(d, i) { return d.node.width/2; })
.attr("height", function(d, i) { return d.node.height/2; });
// .append("text")
// .text(function(d, i) { return i % 2 == 0 ? "" : d.node.id; })
// .style("fill", "#555")
// .style("font-family", "Arial")
// .style("font-size", 12)
// .style("pointer-events", "none");
node.on("mouseover", focus).on("mouseout", unfocus);
node.call(
d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
node.on("mouseover", focus).on("mouseout", unfocus);
function ticked() {
node.call(updateNode);
link.call(updateLink);
labelLayout.alphaTarget(0.3).restart();
labelNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 16;
this.setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
labelNode.call(updateNode);
}
function fixna(x) {
if (isFinite(x)) return x;
return 0;
}
function focus(d) {
var index = d3.select(d3.event.target).datum().index;
node.style("opacity", function(o) {
return neigh(index, o.index) ? 1 : 0.1;
});
labelNode.attr("display", function(o) {
return neigh(index, o.node.index) ? "block": "none";
});
link.style("opacity", function(o) {
return o.source.index == index || o.target.index == index ? 1 : 0.1;
});
}
function unfocus() {
labelNode.attr("display", "block");
node.style("opacity", 1);
link.style("opacity", 1);
}
function updateLink(link) {
link.attr("x1", function(d) { return fixna(d.source.x); })
.attr("y1", function(d) { return fixna(d.source.y); })
.attr("x2", function(d) { return fixna(d.target.x); })
.attr("y2", function(d) { return fixna(d.target.y); });
}
function updateNode(node) {
node.attr("transform", function(d) {
return "translate(" + fixna(d.x) + "," + fixna(d.y) + ")";
});
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
if (!d3.event.active) graphLayout.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) graphLayout.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}); // d3.json
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment