Published
Edited
May 12, 2021
10 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
get_node_links_from(["Kenji Inoue", "Tatsuo Arai"], [1970, 2021], [0, 1000])
Insert cell
Insert cell
Insert cell
Insert cell
// initial_people = get_random_authors(10)
initial_people = ["Eugenio Guglielmelli", "Tatsuo Arai"]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const width = 600;
const height = 400;

// set of the "clicked" nodes to show, does not contain neighbors of clicked nodes
let clicked_people = new Set(initial_people);

const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);

// Allow zoom/panning
const g = svg.append("g").attr("class", "everything"); // encompassing group
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 });
}
Insert cell
chart.update(nodeFilter, edgeFilter, yearFilter, citationFilter)
Insert cell
html`<style>
circle {
cursor: pointer;
fill: #444;
stroke: #fff;
stroke-width: 0.7;
transition: 0.3s fill, 0.3s stroke;
}
circle.exit {
stroke-opacity: 0;
}
circle.clicked, circle.clicked:hover {
fill: #ffa600;
}
circle.neighbor {
fill: #47e26f;
}
circle:hover {
stroke: #47e26f;
fill: #444;
}

line {
/* stroke: #111; */
stroke: #5151d3;
stroke-width: 0.5;
transition: 0.3s;
}
line.highlight {
stroke: #47e26f;
}
line.exit {
stroke-opacity: 0;
stroke: #fff;
}

.tooltip {
stroke: #fff;
stroke-width: 5;
stroke-opacity: 0.9;
paint-order: stroke;

text-anchor: middle;
font-size: 12px;
font-family: Helvetica, sans-serif;
pointer-events: none;
}
</style>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more