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