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

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