Public
Edited
Jun 4, 2024
Insert cell
Insert cell
predefined = {
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", "lightblue")
.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);

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

// Helper functions for animations
function animateNodes(nodesToAnimate, duration, onComplete) {
gsap.to(nodesToAnimate, {
duration: duration,
opacity: 1,
ease: "power1.inOut",
onComplete: onComplete
// stagger: 0.01
});
}

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

// Identify first and second-level nodes
const firstLevelNodeIds = new Set();
const secondLevelNodeIds = new Set();

links.forEach((link) => {
firstLevelNodeIds.add(link.source.productId);
if (!firstLevelNodeIds.has(link.target.productId)) {
secondLevelNodeIds.add(link.target.productId);
}
});

// Select first and second-level nodes and links
const firstLevelNodes = svg
.selectAll(".node")
.filter((d) => firstLevelNodeIds.has(d.productId))
.nodes();
const firstLevelLinks = svg
.selectAll(".link")
.filter((d) => firstLevelNodeIds.has(d.source.productId))
.nodes();
const secondLevelNodes = svg
.selectAll(".node")
.filter((d) => secondLevelNodeIds.has(d.productId))
.nodes();

// Function to create the initial state of links for animation
function setInitialLinkState() {
svg.selectAll(".link").each(function () {
const linkElement = d3.select(this);
const totalLength = calculateLinkLength(linkElement.data()[0]);
linkElement
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength);
});
}

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

// Initialize the initial state for links
setInitialLinkState();

// Create a GSAP timeline
const tl = gsap.timeline();

// Animate first-level nodes
tl.to(
firstLevelNodes,
{
duration: 1,
opacity: 1,
ease: "power1.inOut"
},
"start"
);

// Animate links
firstLevelLinks.forEach((linkElement) => {
const totalLength = calculateLinkLength(d3.select(linkElement).data()[0]);
tl.fromTo(
linkElement,
{ strokeDashoffset: totalLength },
{
duration: 2,
strokeDashoffset: 0,
ease: "power1.inOut"
},
"start+=1" // Start this animation 1 second after the first-level nodes begin
);
});

// Animate second-level nodes
tl.to(
secondLevelNodes,
{
duration: 1,
opacity: 1,
ease: "power1.inOut"
},
"start+=2.5"
); // Adjust the time to start after the links animation

// Helper function to calculate link length

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