networkGraph = (data, options = {key}) => {
let { height, type, layoutIterations } = options;
if (!height) height = window.screen.availHeight;
if (!type) type = "undireceted";
if (!layoutIterations) layoutIterations = 64;
const nodes = data.nodes.map((d) => ({ id: d.key, ...d.attributes }));
const links = data.edges.map((d) => ({
id: d.key,
source: d.source,
target: d.target,
...d.attributes
}));
data.spatialized = {
links,
nodes
};
const thickness = d3.scaleLinear(
[1, d3.max(links, (d) => d.weight)],
[1, 10]
);
const label_size = 12;
let new_label_size = label_size,
label_size_hover = label_size * 1.3;
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3.forceLink(links).id((d) => d.id)
)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collision",
d3.forceCollide((d) => size(d.count) + 0.5)
)
.stop()
.tick(layoutIterations);
//.on("tick", ticked)
//.on("end", () => { label.attr("display", "block"); });
// Create the SVG container.
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, window.screen.availWidth, height])
.style("background-color", "#fafafa");
const g = svg.append("g");
// Add a line for each link, and a circle for each node.
const link = g
.append("g")
.selectAll()
.data(links)
.join(type === "directed" ? "path" : "line")
.attr("fill", "none")
.attr("stroke-width", (d) => thickness(d.weight))
.attr("stroke", "#999")
.attr("stroke-opacity", 0.25);
const node = g
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.selectAll()
.data(nodes)
.join("circle")
.attr("r", (d) => {
d.size = size(d.count);
return d.size;
})
.attr("fill", (d) => {
d.color = color(d.group);
return d.color;
})
.style("cursor", "pointer")
.on("mouseenter", function (event) {
const id = event.target.__data__.id;
const connected = [];
const filteredLinks = links.filter(
(e) => e.source.id == id || e.target.id == id
);
filteredLinks.forEach((e) => {
if (connected.indexOf(e.source) < 0) {
connected.push(e.source.id);
}
if (connected.indexOf(e.target) < 0) {
connected.push(e.target.id);
}
});
node.filter((d) => connected.indexOf(d.id) < 0).attr("fill", "white");
link
.attr("stroke-opacity", 0.25)
.filter((e) => e.source.id == id || e.target.id == id)
.attr("stroke-opacity", 1);
label
.style("opacity", 0.25)
.filter((d) => connected.indexOf(d.id) > -1)
.style("opacity", 1)
.attr("fill", "#000")
.filter((d) => d.id === event.target.__data__.id)
.attr("font-weight", "bold")
.attr("font-size", label_size_hover)
.raise();
})
.on("mouseleave", function (event) {
node.attr("fill", (d) => color(d.group));
link.attr("stroke-opacity", 0.25);
label
.style("opacity", 1)
.attr("fill", "#666")
.attr("font-weight", "normal")
.attr("font-size", new_label_size);
})
.on("click", function (event) {
const d = event.target.__data__;
if (d.group === "post") {
window.open(`https://www.instagram.com/p/${d.id}`, "_blank").focus();
} else if (d.group === "account" || d.group === "new account") {
window.open(`https://www.instagram.com/${d.id}`, "_blank").focus();
} else if (d.group === "hashtag") {
window
.open(
`https://www.instagram.com/explore/tags/${d.id.replace(/^#/g, "")}`,
"_blank"
)
.focus();
}
});
const label = g
.append("g")
.selectAll()
.data(nodes)
.join("text")
// .attr("display", "none")
.attr("fill", "#666")
.attr("font-family", "sans-serif")
.attr("font-size", new_label_size)
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.text((d) => d.label);
node.append("title").text((d) => d.id + "-" + d.count);
link.append("title").text((d) => d.id + "-" + d.weight);
ticked();
// Add a drag behavior.
node.call(
d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended)
);
// links are drawn as curved paths between nodes,
// through the intermediate nodes
function positionLink(d) {
var offset = 20;
var midpoint_x = (d.source.x + d.target.x) / 2;
var midpoint_y = (d.source.y + d.target.y) / 2;
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var normalise = Math.sqrt(dx * dx + dy * dy);
var offSetX = midpoint_x + offset * (dy / normalise);
var offSetY = midpoint_y - offset * (dx / normalise);
return (
"M" +
d.source.x +
"," +
d.source.y +
"S" +
offSetX +
"," +
offSetY +
" " +
d.target.x +
"," +
d.target.y
);
}
// Set the position attributes of links and nodes each time the simulation ticks.
function ticked() {
if (type === "directed") {
link.attr("d", positionLink);
} else {
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);
label.attr("x", (d) => d.x).attr("y", (d) => d.y);
}
// Reheat the simulation when drag starts, and fix the subject position.
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
// Update the subject (dragged node) position during drag.
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
// Restore the target alpha so the simulation cools after dragging ends.
// Unfix the subject position now that it’s no longer being dragged.
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
// When this cell is re-run, stop the previous simulation. (This doesn’t
// really matter since the target alpha is zero and the simulation will
// stop naturally, but it’s a good practice.)
invalidation.then(() => simulation.stop());
svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.scaleExtent([0.1, 8])
.on("zoom", zoomed)
);
function zoomed({ transform }) {
g.attr("transform", transform);
new_label_size = label_size / transform.k;
label_size_hover = new_label_size * 1.3;
label.attr("font-size", new_label_size);
}
return svg.node();
}