// const key = "/" + nodes.map((n) => n.name).join("/");
// cache[key] = { points };
// });
// // Create a container for particles first,
// // to keep particles below the labels which are declared next
// const particlesContainer = g.append("g");
// // Labels
// //
// g.selectAll(".label")
// .data(sankey.nodes) // `.slice(1)` to skip the root node
// .join("g")
// .attr("class", "label")
// .attr(
// "transform",
// (d) => `translate(${d.x1 - bandHeight / 2}, ${d.y0 + bandHeight / 2})`
// )
// .attr("dominant-baseline", "middle")
// .attr("text-anchor", "end")
// // This is how we make labels visible on multicolor background
// // we create two <text> with the same label
// .call((label) =>
// label
// .append("text")
// // the lower <text> serves as outline to make contrast
// .attr("stroke", "white")
// .attr("stroke-width", 3)
// .style("font-family", "Verdana")
// .text((d) => d.name)
// )
// // the upper <text> is the actual label
// .call((label) =>
// label
// .append("text")
// .attr("fill", "#444")
// .style("font-family", "Verdana")
// .text((d) => d.name)
// );
// // Counters
// //
// const counters = g
// .selectAll(".counter")
// .data(leaves)
// .join("g")
// .attr("class", "counter")
// .attr("transform", (d) => `translate(${width - margin.left}, ${d.node.y0})`)
// .each(function (leaf, i) {
// d3.select(this)
// .selectAll(".group")
// .data(["males", "females"])
// .join("g")
// .attr("class", "group")
// .attr("transform", (d, i) => `translate(${-i * 60}, 0)`)
// // Align coutners to the right, because running numbers are easier for the eye to compare this way
// .attr("text-anchor", "end")
// // Use monospaced font to keep digits aligned as they change during the animation
// .style("font-family", "Arial") //Menlo
// // Add group titles only once, on the top
// .call(
// (g) =>
// i === 0 &&
// g
// .append("text")
// .attr("dominant-baseline", "hanging")
// .attr("fill", "#999")
// .style("font-size", 9)
// .style("text-transform", "uppercase")
// .style("letter-spacing", 0.7) // a rule of thumb: increase letter spacing a bit, when use uppercase
// .text((d) => d)
// )
// // Absolute counter values
// // .call(
// // (g) =>
// // g
// // .append("text")
// // .attr("class", "absolute")
// // .attr("fill", (d) => colorScale(d))
// // .attr("font-size", 20)
// // .attr("dominant-baseline", "middle")
// // .attr("y", bandHeight / 2 - 2)
// // .text(0) // will be updated during the animation
// // )
// // Percentage counter values
// .call(
// (g) =>
// g
// .append("text")
// .attr("class", "percent")
// .attr("dominant-baseline", "middle") //hanging
// .attr("fill", "#999")
// .attr("font-size", 20)
// .attr("y", bandHeight / 2 + 9)
// .text("0%") // will be updated during the animation
// );
// });
// // Instead of `return svg.node()` we do this trick.
// // It's needed to expose `update` function outside of this cell.
// // It's Observable-specific, you can see more animations technics here:
// // https://observablehq.com/@d3/learn-d3-animation?collection=@d3/learn-d3
// //
// return Object.assign(svg.node(), {
// // update will be called on each tick, so here we'll perform our animation step
// update(t) {
// // add particles if needed
// //
// addParticlesMaybe(t);
// // update counters
// //
// counters.each(function (d) {
// const finished = particles
// .filter((p) => p.target.name === d.node.name)
// .filter((p) => p.pos >= p.length);
// d3.select(this)
// .selectAll(".group")
// .each(function (group) {
// const count = finished.filter(
// (p) => p.target.group === group
// ).length;
// d3.select(this).select(".absolute").text(count);
// d3.select(this)
// .select(".percent")
// .text(d3.format(".0%")(count / totalParticles));
// });
// });
// // move particles
// //
// particlesContainer
// .selectAll(".particle")
// .data(
// particles.filter((p) => p.pos < p.length),
// (d) => d.id
// )
// .join(
// (enter) =>
// enter
// .append("rect")
// .attr("class", "particle")
// .attr("opacity", 0.8)
// .attr("fill", (d) => d.color)
// .attr("width", psize)
// .attr("height", psize),
// (update) => update,
// (exit) => exit
// //.remove() // uncomment to remove finished particles
// )
// // At this point we have `cache` with all possible coordinates.
// // We just need to figure out which exactly coordinates to use at time `t`
// //
// .each(function (d) {
// // every particle appears at its own time, so adjust the global time `t` to local time
// const localTime = t - d.createdAt;
// d.pos = localTime * d.speed;
// // extract the current and the next point coordinates from the precomputed cache
// const index = Math.floor(d.pos);
// const coo = cache[d.target.path].points[index];
// const nextCoo = cache[d.target.path].points[index + 1];
// if (coo && nextCoo) {
// // `index` is integer, but `d.pos` is float, so there are ticks when a particle is
// // between the two precomputed points. We use `delta` to compute position between the current
// // and the next coordinates to make the animation smoother
// const delta = d.pos - index; // try to set it to 0 to see how jerky the animation is
// const x = coo.x + (nextCoo.x - coo.x) * delta;
// const y = coo.y + (nextCoo.y - coo.y) * delta;
// // squeeze particles when they close to finish
// const lastX = cache[d.target.path].points[d.length - 1].x;
// const squeezeFactor = Math.max(0, psize - (lastX - x)); // gets from 0 to `psize`, when finish
// const h = Math.max(2, psize - squeezeFactor); // gets from `psize` to 2
// const dy = (psize - h) / 2; // increases as the particle squeezes, to keep it centered
// const w = psize + squeezeFactor; // the width increses twice, when finish
// const dx = squeezeFactor / 2; // compensates x position when the width increases
// d3.select(this)
// .attr("x", x - dx)
// .attr("y", y + d.offset + dy)
// .attr("height", h)
// .attr("width", w);
// }
// });
// }
// });
// }