Public
Edited
Jan 8, 2024
2 forks
Insert cell
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
{
console.log(
nodes.slice(0,3)
)
console.log(
links.slice(0,3)
)
}
Insert cell
predefined = {
const quadtree = d3
.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
const tooltip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("background", "white")
.style("border", "1px solid #CCCCCC")
.style("border-radius", "3px")
.style("padding", "10px")
.text("tooltip");
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", 0.61); // 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", 1); // 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);
function debounce(func, timeout = 100) {
let timer;
return function (event) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
// Apply the 'this' context and pass the event argument to the original function
func.call(this, event);
}, timeout);
};
}

// Function to handle mousemove with quadtree search
function handleMouseMove(event) {
// Find the closest node to the mouse using the quadtree
const [mx, my] = d3.pointer(event, svg.node());
const radius = 5; // Define a search radius
const closest = quadtree.find(mx, my, radius);

if (closest) {
// If a node is found within the radius, show the tooltip
tooltip
.style("visibility", "visible")
.style("top", event.pageY - 10 + "px")
.style("left", event.pageX + 10 + "px")
.html(`Product ID: ${closest.productId}`);
} else {
// If no node is found, hide the tooltip
tooltip.style("visibility", "hidden");
}
}

// Apply the debounced function to the mousemove event
svg.on("mousemove", debounce(handleMouseMove, 250));
const allTargets = new Set(links.map((link) => link.target.productId));
const graph = {};
links.forEach((link) => {
const sourceId = link.source.productId;
const targetId = link.target.productId;

if (!graph[sourceId]) {
graph[sourceId] = { nodeId: sourceId, targets: [] };
}
if (!graph[targetId]) {
graph[targetId] = { nodeId: targetId, targets: [] };
}

graph[sourceId].targets.push(graph[targetId]);
});

// Step 2: Traverse the graph to find the "source-target-source-target..." sequence
// This assumes there are no cycles in the graph and it's more like a tree structure
const sequence = [];
const visited = new Set();

function traverse(node) {
if (visited.has(node.nodeId)) return;
visited.add(node.nodeId);
sequence.push(node);

node.targets.forEach((targetNode) => {
sequence.push({ source: node, target: targetNode });
traverse(targetNode);
});
}

// Find an arbitrary start node (e.g., a node with no incoming links)
const startNode = graph[Object.keys(graph).find((id) => !allTargets.has(id))];
traverse(startNode);

// Step 3: Animate based on sequence
function animateSequence(sequence, index = 0) {
if (index >= sequence.length) return;

const item = sequence[index];
const nextItem = sequence[index + 1];

if (item.source && item.target) {
// This item is a link
animateLink(item.source, item.target, () =>
animateSequence(sequence, index + 1)
);
} else {
// This item is a node
animateNode(item, () => animateSequence(sequence, index + 1));
}
}

// Helper functions to animate nodes and links
function animateNode(node, onComplete) {
const nodeElement = svg
.selectAll(".node")
.nodes()
.find((n) => n.productId === node.nodeId);
gsap.to(nodeElement, {
duration: 1,
opacity: 1,
ease: "power1.inOut",
onComplete: onComplete
});
}

function animateLink(source, target, onComplete) {
const linkElement = svg
.selectAll(".link")
.nodes()
.find(
(l) =>
l.source.productId === source.nodeId &&
l.target.productId === target.nodeId
);
const totalLength = calculateLinkLength(source, target);

gsap.fromTo(
linkElement,
{ strokeDasharray: `0,${totalLength}` },
{
duration: 2,
strokeDasharray: `${totalLength},${totalLength}`,
ease: "power1.inOut",
onComplete: onComplete
}
);
}

// Helper function to calculate link length (you may already have this logic in your SVG drawing code)
function calculateLinkLength(source, target) {
const dx = target.x - source.x;
const dy = target.y - source.y;
return Math.sqrt(dx * dx + dy * dy);
}

// Begin the animation sequence
animateSequence(sequence);
return svg.node();
}
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

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