Published
Edited
Mar 24, 2021
1 star
Insert cell
Insert cell
{
// calculate nodes and links for network
const margin = {top: 20, bottom: 20}
const colorScale = d3.scaleOrdinal(d3.schemePaired)
const rScale = d3.scaleSqrt(
d3.extent(movies, d => d.cast), // domain
[10, 30] // range
)
const yScale = d3.scaleTime(
d3.extent(movies, d => d.date),
[margin.top, height - margin.bottom],
)
const today = new Date()
// go through each movie and color by its series
// fill if the movie has been released
// size by number of characters
// y-position by date of release
const nodes = _.map(movies, d => {
return {
id: d.id,
title: d.title,
color: colorScale(d.series),
r: rScale(d.cast),
fill: today > d.date,
// fy: yScale(d.date)
}
})
console.log(nodes)
const links = {}
_.each(characters, ({movies}) => {
// for each set of movies that a character is in
// create a link between those two movies
_.each(movies, (source) => {
_.each(movies, (target) => {
if (source === target) return
const id = `${source.id},${target.id}`
if (!links[id]) {
links[id] = {
id,
source: source.id,
target: target.id,
weight: 0,
}
}
links[id].weight += 1
})
})
})
return chart(nodes, _.values(links))
}
Insert cell
Insert cell
chart = (nodes, links) => {
const maxLinkWeight = d3.max(links, d => d.weight)
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links)
.id(d => d.id)
.strength(d => d.weight / maxLinkWeight)
.distance(200)
)
.force("charge", d3.forceManyBody().strength(-500))
// .force('y', d3.forceY(d => d.focusY))
.force("center", d3.forceCenter(width / 2, height / 2));

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

const link = svg.append("g")
.attr('id', 'links')
.attr("stroke-opacity", 0.25)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke", d => d.source.color)
.attr("stroke-width", d => d.weight)

const node = svg.append("g")
.attr('id', 'nodes')
.selectAll("g")
.data(nodes)
.join("g")
.call(drag(simulation))
node.append('circle')
.attr("r", d => d.r)
.attr("stroke", d => !d.fill ? d.color : '#999')
.attr('fill', d => d.fill ? d.color : '#fff')
.attr("stroke-width", 1.5);

node.append("text")
.attr('text-anchor', 'middle')
.style('font-size', 10)
.style('font-family', 'monospace')
.text(d => d.title);

simulation.on("tick", () => {
// bound the nodes
_.each(nodes, d => {
d.x = Math.max(d.r, Math.min(width - d.r, d.x))
d.y = Math.max(d.r, Math.min(height - d.r, d.y))
})
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', d => `translate(${d.x}, ${d.y})`);
});

invalidation.then(() => simulation.stop());

return svg.node();
}
Insert cell
md`
## # data & third party libraries
`
Insert cell
movies = FileAttachment("movies.json").json()
.then(data => {
_.each(data, d => d.date = new Date(d.date))
return data
})
Insert cell
characters = FileAttachment("characters.json").json()
Insert cell
d3 = require('d3')
Insert cell
_ = require('lodash')
Insert cell
import {drag} from '@d3/force-directed-graph'
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