Public
Edited
Jan 9, 2024
Insert cell
Insert cell
predefined = {
const nodeMap = new Map(nodes.map((node) => [node.productId, node]));

// Create sequences of node-link-node
const sequences = links.map((link) => {
return {
sourceNode: nodeMap.get(link.source.productId),
link: link,
targetNode: nodeMap.get(link.target.productId)
};
});
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
// Add a line for each link, and a circle for each node.
const link = svg
.append("g")
.attr("stroke", "#CCCCCC")
.attr("stroke-opacity", 0.61)
.selectAll()
.data(links)
.join("line")
.attr("stroke-width", 2)
.attr("class", "link")
.style("opacity", 1); // Initial opacity set to 0

const node = svg
.append("g")
.attr("stroke", "#CCCCCC")
.attr("stroke-width", 1.5)
.selectAll()
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", "white")
.attr("class", "node")
.style("opacity", 0); // Initial opacity set to 0

// original tooltip text
// node.append("title").text((d) => d.productId);
link
.attr("x1", (d) => {
// console.log(d.source);
return d.source.x;
})
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
// .style("mix-blend-mode", "multiply");

node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
// Calculate link length function
function calculateLinkLength(linkData) {
const dx = linkData.target.x - linkData.source.x;
const dy = linkData.target.y - linkData.source.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Function to group sequences into clusters
function groupIntoClusters(sequences, clusterSize) {
const clusters = [];
for (let i = 0; i < sequences.length; i += clusterSize) {
clusters.push(sequences.slice(i, i + clusterSize));
}
return clusters;
}

// Group sequences into clusters of a specified size
const clusterSize = 300; // Adjust this to control the number of sequences per cluster
const clusters = groupIntoClusters(sequences, clusterSize);

// Initialize GSAP timeline
const tl = gsap.timeline();

// Set initial state for links
svg.selectAll(".link").each(function () {
const totalLength = calculateLinkLength(d3.select(this).data()[0]);
d3.select(this)
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength);
});

// Adjusted duration values for faster animation
const nodeDuration = 0.6;
const linkDuration = 1.2;
const clusterStaggerTime = 0.5; // Time between the start of each cluster

// Animate each cluster
clusters.forEach((cluster, clusterIndex) => {
cluster.forEach((seq) => {
const clusterStartTime = clusterIndex * clusterStaggerTime;

// Animate source node
tl.to(
svg
.selectAll(".node")
.filter((d) => d === seq.sourceNode)
.nodes(),
{
duration: nodeDuration,
opacity: 1,
ease: "power1.inOut"
},
clusterStartTime
);

// Animate link
tl.fromTo(
svg
.selectAll(".link")
.filter((d) => d === seq.link)
.nodes(),
{
strokeDashoffset: calculateLinkLength(seq.link)
},
{
duration: linkDuration,
strokeDashoffset: 0,
ease: "power1.inOut"
},
clusterStartTime + nodeDuration
);

// Animate target node
tl.to(
svg
.selectAll(".node")
.filter((d) => d === seq.targetNode)
.nodes(),
{
duration: nodeDuration,
opacity: 1,
ease: "power1.inOut"
},
clusterStartTime + nodeDuration + linkDuration
);
});
});

// Return SVG node
return svg.node();
}
Insert cell
Insert cell
// data = FileAttachment("miserables.json").json()
Insert cell
Insert cell
Insert cell
new Set(metaData.map((d) => d.productSector.productId))
Insert cell
Insert cell
Insert cell
Insert cell
nodes = {
const nodes = data.nodes.map((d) => ({ ...d }));
nodes.forEach((d) => {
d.x = xScale(d.x);
d.y = yScale(d.y);
});

return nodes;
}
Insert cell
Insert cell
// chart = {
// // Specify the dimensions of the chart.

// const height = 1200;

// // Specify the color scale.
// const color = d3.scaleOrdinal(d3.schemeCategory10);

// // The force simulation mutates links and nodes, so create a copy
// // so that re-evaluating this cell produces the same result.
// const links = data.links.map((d) => ({ ...d }));
// const nodes = data.nodes.map((d) => ({ ...d }));
// nodes.forEach((node) => {
// node.x = 0.1 * width;
// node.y = 0.1 * height;
// });

// // Create the simulation with adjusted forces
// const simulation = d3
// .forceSimulation(nodes)
// .force(
// "link",
// d3.forceLink(links).id((d) => d.productId)
// )
// .force("charge", d3.forceManyBody().strength(-30)) // Adjusted charge strength
// .force("center", d3.forceCenter(width / 2, height / 2))
// .stop()
// .tick(n_frames_to_simulate);

// // Create the SVG container.
// const svg = d3
// .create("svg")
// .attr("width", width)
// .attr("height", height)
// .attr("viewBox", [0, 0, width, height])
// .attr("style", "max-width: 100%; height: auto;");

// // Add a line for each link, and a circle for each node.
// const link = svg
// .append("g")
// .attr("stroke", "#CCCCCC")
// .attr("stroke-opacity", 0.6)
// .selectAll()
// .data(links)
// .join("line")
// .attr("stroke-width", 3)
// .attr("class", "link")
// .style("opacity", 0); // Initial opacity set to 0

// const node = svg
// .append("g")
// .attr("stroke", "#CCCCCC")
// .attr("stroke-width", 1.5)
// .selectAll()
// .data(nodes)
// .join("circle")
// .attr("r", 5)
// .attr("fill", "lightblue")
// .attr("class", "node")
// .style("opacity", 0); // Initial opacity set to 0

// node.append("title").text((d) => d.productId);
// 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);
// node
// .transition()
// .duration(7500) // Duration of fade-in
// .delay((d, i) => i * 5) // Stagger the fade-in of nodes
// .style("opacity", 1);

// link
// .transition()
// .duration(7500)
// .delay((d, i) => i * 5)
// .style("opacity", 1);

// // Default styles
// const defaultNodeStyle = { stroke: "#fff", strokeWidth: 1.5 };
// const defaultLinkStyle = {
// stroke: "#999",
// strokeWidth: 3
// };

// // Highlighted styles
// const highlightStyle = { stroke: "red", strokeWidth: 3 };

// node.on("mouseover", handleMouseOver).on("mouseout", handleMouseOut);

// function handleMouseOver(event, d) {
// // Highlight the node with transition
// d3.select(this)
// .transition()
// .duration(transitionTime) // Duration in milliseconds
// .style("stroke", highlightStyle.stroke)
// .style("stroke-width", highlightStyle.strokeWidth);

// // Highlight the links and connected nodes with transition
// link
// .transition()
// .duration(transitionTime)
// .style("stroke", (l) =>
// l.source === d || l.target === d
// ? highlightStyle.stroke
// : defaultLinkStyle.stroke
// )
// .style("stroke-width", (l) =>
// l.source === d || l.target === d
// ? highlightStyle.strokeWidth
// : defaultLinkStyle.strokeWidth(l)
// );

// node
// .transition()
// .duration(transitionTime)
// .style("stroke", (n) =>
// isConnected(d, n) ? highlightStyle.stroke : defaultNodeStyle.stroke
// )
// .style("stroke-width", (n) =>
// isConnected(d, n)
// ? highlightStyle.strokeWidth
// : defaultNodeStyle.strokeWidth
// );

// // Show tooltip
// showTooltip(event, d);
// }

// function handleMouseOut(event, d) {
// // Revert to default styles with transition
// link
// .transition()
// .duration(transitionTime / 2)
// .style("stroke", defaultLinkStyle.stroke)
// .style("stroke-width", defaultLinkStyle.strokeWidth);

// node
// .transition()
// .duration(transitionTime / 2)
// .style("stroke", defaultNodeStyle.stroke)
// .style("stroke-width", defaultNodeStyle.strokeWidth);

// // Hide tooltip
// hideTooltip();
// }

// function isConnected(a, b) {
// return links.some(
// (l) =>
// (l.source === a && l.target === b) || (l.source === b && l.target === a)
// );
// }
// function showTooltip(event, d) {
// const tooltip = d3
// .select("body")
// .append("div")
// .attr("class", "tooltip")
// .style("opacity", 0);

// tooltip.transition().duration(200).style("opacity", 0.9);

// tooltip
// .html("Target: " + d.Target) // Adjust according to your data structure
// .style("left", event.pageX + "px")
// .style("top", event.pageY - 28 + "px");
// }

// function hideTooltip() {
// d3.select(".tooltip").remove();
// }

// // 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());

// return svg.node();
// }
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gsap = gs.gsap
Insert cell
gs = require("https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more