chart = {
const width = 600;
const height = 400;
let clicked_people = new Set(initial_people);
const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);
const g = svg.append("g").attr("class", "everything");
d3
.zoom()
.scaleExtent([1 / 4, 4])
.on("zoom", (evt) => g.attr("transform", evt.transform))(svg);
const tooltip = svg
.append("text")
.attr("class", "tooltip")
.attr("x", 0)
.attr("y", 0);
let link = g.append("g").attr("class", "lines").selectAll("line");
let node = g.append("g").attr("class", "nodes").selectAll("circle");
let simulation = d3
.forceSimulation()
.force(
"link",
d3
.forceLink()
.id((d) => d.id)
.distance(10)
)
.force("charge", d3.forceManyBody().strength(-80).distanceMax(150))
.force(
"collision",
d3.forceCollide().radius((d) => Math.sqrt(d.numPapers))
)
// .force("center", d3.forceCenter(0, 0).strength(0.02))
.force("x", d3.forceX().strength(0.02))
.force("y", d3.forceY().strength(0.02))
.velocityDecay(0.7)
.alphaDecay(0.03)
.on("tick", () => {
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);
});
invalidation.then(() => simulation.stop());
function update(
[minNode, maxNode],
[minEdge, maxEdge],
[minYear, maxYear],
[minCitations, maxCitations]
) {
let { nodes, links } = get_node_links_from(
clicked_people,
[minYear, maxYear],
[minCitations, maxCitations]
);
// Make a shallow copy to protect against mutation, while
// recycling old nodes to preserve position and velocity.
const old = new Map(node.data().map((d) => [d.id, d]));
const nodeSet = new Set(
nodes
.filter((d) => d.numPapers >= minNode && d.numPapers <= maxNode)
.map((d) => d.id)
);
nodes = nodes
.filter((d) => nodeSet.has(d.id))
.map((d) => Object.assign(old.get(d.id) || {}, d));
links = links
.filter(
(d) =>
d.numPapers >= minEdge &&
d.numPapers <= maxEdge &&
nodeSet.has(d.source) &&
nodeSet.has(d.target)
)
.map((d) => Object.assign({}, d));
// When an author node is clicked, toggle showing its neighbors and update
function click(evt, d) {
if (evt.defaultPrevented) return;
if (clicked_people.size > 1 && clicked_people.has(d.id)) {
clicked_people.delete(d.id);
} else {
clicked_people.add(d.id);
}
update(
[minNode, maxNode],
[minEdge, maxEdge],
[minYear, maxYear],
[minCitations, maxCitations]
);
}
// When an author node is hovered over, highlight neighbors and edges
function hoverOn(_evt, d) {
link.classed(
"highlight",
(e) => e.source.id === d.id || e.target.id === d.id
);
const neighbors = get_neighbors_of(d.id, [minYear, maxYear]);
node.classed("neighbor", (e) => neighbors.has(e.id));
tooltip
.text(`${d.id} (${d.numPapers})`)
.transition()
.duration(200)
.style("opacity", 1);
}
// Author node is unhovered, unhighlight all edges and nodes
function hoverOut(_evt, _d) {
link.classed("highlight", false);
node.classed("neighbor", false);
tooltip.transition().duration(500).style("opacity", 0);
}
function hoverMove(evt, _d) {
let [x, y] = d3.pointer(evt);
tooltip.attr("transform", `translate(${x},${y})`);
}
node = node
.data(nodes, (d) => d.id)
.join(
(enter) =>
enter // animate fading in, growing circle
.append("circle")
.classed("clicked", (d) => clicked_people.has(d.id))
.attr("x", (Math.random() - 0.5) * width)
.attr("y", (Math.random() - 0.5) * height)
.attr("r", 1)
.style("opacity", 0.5)
.transition()
.duration(800)
.attr("r", (d) => Math.sqrt(d.numPapers))
.style("opacity", 1)
.selection(),
(update_) =>
update_
.attr("x", (Math.random() - 0.5) * width)
.attr("y", (Math.random() - 0.5) * height)
.classed("clicked", (d) => clicked_people.has(d.id))
.attr("r", (d) => Math.sqrt(d.numPapers)),
(exit) =>
exit // animate fading out, shrinking circle
.classed("exit", true)
.classed("neighbor", false)
.transition()
.duration(900)
.style("opacity", 0)
.attr("r", 0)
.remove()
);
node
.on("click", click)
.on("mouseover", hoverOn)
.on("mouseout", hoverOut)
.on("mousemove", hoverMove)
.call(drag(simulation));
link = link.data(links).join(
(enter) =>
enter
.append("line")
.style("stroke-opacity", 0)
.transition()
.duration(800)
.style("stroke-opacity", (d) => Math.min(1, d.numPapers / 10))
.selection(),
(update_) => update_,
(exit) =>
exit
.classed("exit", true)
.classed("highlight", false)
.transition()
.duration(800)
.style("stroke", "#fff")
.style("opacity", 0)
.remove()
);
// Add titles
// node.append("title").text((d) => `${d.id} (${d.numPapers})`);
link
.append("title")
.text((d) => `${d.source} - ${d.target} (${d.numPapers})`);
simulation.nodes(nodes);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
return Object.assign(svg.node(), { update });
}