Public
Edited
Mar 5
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
getPropagationMultiplier = (weight, normalizedDist) => {
// linear interpolation from factor=1 at dist=0 to factor=weight at dist=1
return 1 - (1 - weight) * normalizedDist;
// return weight * (1 - normalizedDist);
}
Insert cell
drop = (doiY, x, y) => {
// normalize embedding distance
const embDist = dEmb(x, y) / dEmbMax; // in [0,1]

// restrict embedding distance propagation by embeddingSlider.handle1.
// if embDist > embeddingSlider.handle1, no embedding propagation occurs.
// otherwise, scale distance from [0..embeddingSlider.handle1] down to [0..1].
let embMultiplier = 0;
if (embDist <= embeddingSlider.handle1) {
const scaledEmbDist = embDist / embeddingSlider.handle1;
embMultiplier = getPropagationMultiplier(
embeddingSlider.handle2,
// embDist
scaledEmbDist
);
}

// compute topology distance and multiplier
const topologyDist = distTopology(y, x); // in [0,1] or Infinity
let topologyMultiplier = 0;
if (Number.isFinite(topologyDist) && Math.abs(t(x) - t(y)) <= 1) {
if (t(y) - t(x) == 1) {
topologyMultiplier = wPastFuture.past.handle1;
} else if (t(x) - t(y) == 1) {
topologyMultiplier = wPastFuture.future.handle1;
} else if (t(x) === t(y)) {
topologyMultiplier = 1;
}
}

// multiply each by the DoI (degree of interest) of y
const propagationFromEmbedding = doiY * embMultiplier;
const propagationFromTopology = doiY * topologyMultiplier;

// return the maximum propagation
return Math.max(propagationFromEmbedding, propagationFromTopology);
}
Insert cell
distTopology = (x, y) => {
if (x.type == "node" && y.type == "node") {
// If they are on different trajectories, there is no topological distance
if (trajectory(x) !== trajectory(y)) {
return Infinity;
} else if (t(x) < t(y)) {
// Past distances
return (
(1 - wPastFuture.future.handle1) +
wPastFuture.future.handle1 * ((t(y) - t(x)) / tMax)
);
} else if (t(x) > t(y)) {
// Future distances
return (
(1 - wPastFuture.past.handle1) +
wPastFuture.past.handle1 * ((t(x) - t(y)) / tMax)
);
} else {
// Same timestamp
return 0;
}
} else {
return Infinity;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data = FileAttachment("combination_example_2.json").json()
// data = FileAttachment("guiding_example_fig.json").json()
Insert cell
// chartCatMullRomSize = {
// const width = 600;
// const height = 600;
// const margin = { top: 20, right: 20, bottom: 30, left: 40 };

// const nodeById = new Map(nodes.map((d) => [d.id, d]));

// const x = d3
// .scaleLinear()
// .domain(d3.extent(nodes, (d) => d.coordinate.x))
// .nice()
// .range([margin.left, width - margin.right]);

// const y = d3
// .scaleLinear()
// .domain(d3.extent(nodes, (d) => d.coordinate.y))
// .nice()
// .range([height - margin.bottom, margin.top]);

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

// svg
// .append("defs")
// .selectAll("marker")
// .data(["end"])
// .join("marker")
// .attr("id", String)
// .attr("viewBox", "0 -5 10 10")
// .attr("refX", 15)
// .attr("refY", 0)
// .attr("markerWidth", 2)
// .attr("markerHeight", 2)
// .attr("orient", "auto")
// .append("path")
// .attr("d", "M0,-5L10,0L0,5")
// .attr("fill", "#999");

// // Helper function to compute a Catmull-Rom point
// function catmullRomPoint(t, p0, p1, p2, p3) {
// const t2 = t * t;
// const t3 = t * t * t;

// // Catmull-Rom basis
// const x =
// 0.5 *
// (2 * p1[0] +
// (-p0[0] + p2[0]) * t +
// (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
// (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3);

// const y =
// 0.5 *
// (2 * p1[1] +
// (-p0[1] + p2[1]) * t +
// (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
// (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3);
// return [x, y];
// }

// // For each link, we identify the four control points.
// // Since we only have two main points for a link (source, target),
// // we get neighbors from the trajectoryNodes. If that's not desired,
// // you can handle that differently. For now, let's assume we have a way
// // to handle endpoints by repeating points.

// // If you have trajectories, you can reconstruct them as before.
// // If not, and each link is isolated, you can just duplicate endpoints.
// // For simplicity here, we'll just duplicate endpoints if neighbors are missing.

// // We'll still rely on order by time+id to find trajectories if desired.
// // If you don't have trajectories, ignore this part.
// const linksByTraj = d3.group(links, (d) => d.trajectory);

// const trajectoryNodeLists = new Map();
// for (const [trajId, trajLinks] of linksByTraj) {
// const trajNodeIds = new Set();
// for (const link of trajLinks) {
// trajNodeIds.add(link.source);
// trajNodeIds.add(link.target);
// }
// const trajNodes = Array.from(trajNodeIds, (id) => nodeById.get(id));
// trajNodes.sort((a, b) => {
// let t = d3.ascending(a.time, b.time);
// return t !== 0 ? t : d3.ascending(a.id, b.id);
// });
// trajectoryNodeLists.set(trajId, trajNodes);
// }

// // We'll generate a polyline from sampled points.
// const lineGenerator = d3
// .line()
// .x((d) => d[0])
// .y((d) => d[1])
// // We use a straight line between sampled points because we already computed curvature
// .curve(d3.curveLinear);

// function update() {
// svg
// .selectAll("path.link")
// .data(links, (d) => `${d.source}-${d.target}`)
// .join("path")
// .attr("class", "link")
// .attr("fill", "none")
// .attr("stroke", (d) => (selected(d) ? "#d95f02" : "#999"))
// .attr("stroke-opacity", 0.6)
// .attr("stroke-width", (d) => Math.max(2, 25 * getDoi(d)))
// // .attr("stroke-opacity", (d) => Math.max(0.05, getDoi(d)))
// // .attr("stroke-width", (d) => 5)
// .attr("marker-end", "url(#end)")
// .on("click", (event, d) => {
// click(d);
// update();
// })
// .attr("d", (d) => {
// const trajNodes = trajectoryNodeLists.get(d.trajectory);
// const sourceNode = nodeById.get(d.source);
// const targetNode = nodeById.get(d.target);

// const sourceIndex = trajNodes ? trajNodes.indexOf(sourceNode) : -1;
// const targetIndex = trajNodes ? trajNodes.indexOf(targetNode) : -1;

// // If we have a trajectory, we can pick neighbors.
// // If not found or no trajectory grouping, just duplicate endpoints.
// // p1 = source, p2 = target
// let p1 = [x(sourceNode.coordinate.x), y(sourceNode.coordinate.y)];
// let p2 = [x(targetNode.coordinate.x), y(targetNode.coordinate.y)];

// let p0 = p1;
// let p3 = p2;

// if (trajNodes && sourceIndex >= 0 && targetIndex >= 0) {
// const startIndex = Math.min(sourceIndex, targetIndex);
// const endIndex = Math.max(sourceIndex, targetIndex);
// p1 = [
// x(trajNodes[startIndex].coordinate.x),
// y(trajNodes[startIndex].coordinate.y)
// ];
// p2 = [
// x(trajNodes[endIndex].coordinate.x),
// y(trajNodes[endIndex].coordinate.y)
// ];

// // For p0
// if (startIndex > 0) {
// p0 = [
// x(trajNodes[startIndex - 1].coordinate.x),
// y(trajNodes[startIndex - 1].coordinate.y)
// ];
// } else {
// p0 = p1; // duplicate if none available
// }

// // For p3
// if (endIndex < trajNodes.length - 1) {
// p3 = [
// x(trajNodes[endIndex + 1].coordinate.x),
// y(trajNodes[endIndex + 1].coordinate.y)
// ];
// } else {
// p3 = p2; // duplicate if none available
// }
// } else {
// // No trajectory info, just duplicate endpoints
// p0 = p1;
// p3 = p2;
// }

// // Sample points along t=0 to t=1
// const samples = 20; // number of segments
// const curvePoints = [];
// for (let i = 0; i <= samples; i++) {
// const t = i / samples;
// curvePoints.push(catmullRomPoint(t, p0, p1, p2, p3));
// }

// return lineGenerator(curvePoints);
// });

// const drag = d3.drag().on("drag", function (event, d) {
// const newX = x.invert(event.x);
// const newY = y.invert(event.y);
// d3.select(this)
// .attr("cx", (d.coordinate.x = newX))
// .attr("cy", (d.coordinate.y = newY));
// update();
// click("dummy");
// });

// // Update nodes
// svg
// .selectAll("circle")
// .data(nodes, (d) => d.id)
// .join("circle")
// .attr("cx", (d) => x(d.coordinate.x))
// .attr("cy", (d) => y(d.coordinate.y))
// .attr("r", (d) => Math.max(5, 25 * getDoi(d)))
// // .attr("r", (d) => 10)
// // .attr("opacity", (d) => Math.max(0.05, getDoi(d)))
// .attr("fill", (d) => (selected(d) ? "#d95f02" : "#999"))
// .attr("stroke", "#fff")
// .on("click", (event, d) => {
// click(d);
// update();
// })
// .call(drag);

// // Update node labels
// svg
// .selectAll(".node-label")
// .data(nodes, (d) => d.id)
// .join("text")
// .attr("class", "node-label")
// .attr("x", (d) => x(d.coordinate.x))
// .attr("y", (d) => y(d.coordinate.y) - 10)
// .attr("text-anchor", "middle")
// .attr("font-family", "sans-serif")
// .attr("font-size", 24)
// .attr("fill", "black")
// .attr("stroke", "white")
// .attr("stroke-width", 0.5)
// .text((d) => getDoi(d).toFixed(2));

// // Update edge labels
// svg
// .selectAll(".edge-label")
// .data(links, (d) => `${d.source}-${d.target}`)
// .join("text")
// .attr("class", "edge-label")
// .attr("text-anchor", "middle")
// .attr("font-family", "sans-serif")
// .attr("font-size", 24)
// .attr("fill", "black")
// .attr("stroke", "white")
// .attr("stroke-width", 0.5)
// .attr(
// "x",
// (d) =>
// (x(nodeById.get(d.source).coordinate.x) +
// x(nodeById.get(d.target).coordinate.x)) /
// 2
// )
// .attr(
// "y",
// (d) =>
// (y(nodeById.get(d.source).coordinate.y) +
// y(nodeById.get(d.target).coordinate.y)) /
// 2
// )
// .text((d) => getDoi(d).toFixed(2));
// }

// update();
// click("dummy");

// // return svg.node();
// 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