Public
Edited
Apr 23
1 fork
Insert cell
Insert cell
founders_metadata - Correspondance Pairs (remapped).csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
d3 = require("d3@7")
Insert cell
Insert cell
founders_metadata - Sheet10.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
genderDict = founders_metadataSheet10.reduce((acc, d) => {
acc[d.source] = d.gender;
return acc;
}, {});


Insert cell
filtered_data = founders_metadataSheet10.map(d => ({
source: d.source,
target: d.target,
weight: +d.weight, // Convert weight to a number
gender: genderDict[d.source] // Keep gender for node coloring
}));
Insert cell
nodes = Array.from(
new Set(filtered_data.flatMap(d => [d.source, d.target])),
id => ({
id: id,
gender: filtered_data.find(d => d.source === id || d.target === id)?.gender || "NA"
})
);

Insert cell
Insert cell
chart = {
const width = 800;
const height = 600;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.call(
d3.zoom()
.scaleExtent([0.05, 50]) // Allow zooming between 50% and 500%
.on("zoom", (event) => {
g.attr("transform", event.transform);
})
);

const g = svg.append("g"); // Group for zoomable content

// Create a logarithmic scale for edge weights
const weightScale = d3.scaleLog()
.domain(d3.extent(links, d => d.weight)) // Min and max weights
.range([0.5, 5]); // Scaled line widths

const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));

// Add arrow markers
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerWidth", 5) // Adjust arrow size
.attr("markerHeight", 5) // Adjust arrow size
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#999");

// Add links
const link = g.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => weightScale(d.weight)) // Logarithmic stroke width
.attr("marker-end", "url(#arrow)"); // Add arrows

// Add nodes
const node = g.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", d => {
if (d.gender === "male") return "#008000"; // Green
if (d.gender === "female") return "#FFB347"; // Pastel orange
return "#000000"; // Black for N/A
})
.call(d3.drag()
.on("start", (event, d) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
})
);

// Titles for hover
node.append("title")
.text(d => d.id);

// Update positions dynamically
simulation.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);
});

return svg.node();
}

Insert cell
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(400, 300)); // Adjust

Insert cell
Insert cell
femaleSendersChart = {
const width = 800;
const height = 600;

// Filter nodes to include only female senders
const femaleNodes = nodes.filter(d => d.gender === "female");

// Filter links to include only those where the source is female
const femaleLinks = links.filter(d =>
femaleNodes.some(node => node.id === d.source.id) // Check if source exists in filtered nodes
);

const femaleSvg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.call(
d3.zoom()
.scaleExtent([0.05, 5000000000]) // More zoom out range
.on("zoom", (event) => {
femaleG.attr("transform", event.transform);
})
);

const femaleG = femaleSvg.append("g"); // Group for zoomable content

// Create a logarithmic scale for edge weights
const femaleWeightScale = d3.scaleLog()
.domain(d3.extent(femaleLinks, d => d.weight)) // Min and max weights
.range([0.5, 5]); // Scaled line widths

const femaleSimulation = d3.forceSimulation(femaleNodes)
.force("link", d3.forceLink(femaleLinks).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));

// Add arrow markers
femaleSvg.append("defs").append("marker")
.attr("id", "female-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#999");

// Add links
const femaleLink = femaleG.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(femaleLinks)
.join("line")
.attr("stroke-width", d => femaleWeightScale(d.weight))
.attr("marker-end", "url(#female-arrow)"); // Add arrows

// Add nodes (only female)
const femaleNode = femaleG.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(femaleNodes)
.join("circle")
.attr("r", 5)
.attr("fill", "#ADD8E6") // Light Blue for female
.call(d3.drag()
.on("start", (event, d) => {
if (!event.active) femaleSimulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) femaleSimulation.alphaTarget(0);
d.fx = null;
d.fy = null;
})
);

// Titles for hover
femaleNode.append("title")
.text(d => d.id);

// Update positions dynamically
femaleSimulation.on("tick", () => {
femaleLink
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);

femaleNode
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});

return femaleSvg.node();
}

Insert cell
Insert cell
top_correspondents_10 = Array.from(
d3.rollup(
founders_metadataSheet10.flatMap(d => [
{ name: d.source, weight: +d.weight },
{ name: d.target, weight: +d.weight }
]),
v => d3.sum(v, d => d.weight),
d => d.name
)
)
.sort((a, b) => b[1] - a[1]) // Sort by weight descending
.slice(0, 10) // Keep top 10
.map(d => d[0]); // Extract names
Insert cell
filtered_data_10 = founders_metadataSheet10.filter(
d => top_correspondents_10.includes(d.source) && top_correspondents_10.includes(d.target)
);

Insert cell
nodes_10 = Array.from(
new Set(filtered_data_10.flatMap(d => [d.source, d.target])),
id => ({ id })
);
Insert cell
Insert cell
chart_10 = {
const width = 800;
const height = 600;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.call(
d3.zoom()
.scaleExtent([0.5, 5]) // Zoom scale
.on("zoom", (event) => {
g.attr("transform", event.transform);
})
);

const g = svg.append("g");

// Add arrow markers
svg.append("defs").append("marker")
.attr("id", "arrow10")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("markerWidth", 3)
.attr("markerHeight", 3)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#999");

// Create a logarithmic scale for edge widths
const weightScale = d3.scaleLog()
.domain(d3.extent(links_10, d => d.weight)) // Min and max weights
.range([0.5, 5]); // Range of stroke widths

// Initialize force simulation
const simulation = d3.forceSimulation(nodes_10)
.force("link", d3.forceLink(links_10).id(d => d.id).distance(120))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2));

// Add links
const link = g.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links_10)
.join("line")
.attr("stroke-width", d => weightScale(d.weight)) // Apply logarithmic scale
.attr("marker-end", "url(#arrow10)"); // Add arrows

// Add nodes
const node = g.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes_10)
.join("circle")
.attr("r", 8)
.attr("fill", "#1f77b4")
.call(
d3.drag()
.on("start", (event, d) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
})
);

// Add tooltips
node.append("title").text(d => d.id);

// Add labels for nodes
const labels = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.selectAll("text")
.data(nodes_10)
.join("text")
.text(d => d.id);

// Update positions dynamically
simulation.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);

labels
.attr("x", d => d.x)
.attr("y", d => d.y - 15);
});

return svg.node();
}

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