export default function useD3Demo()

in src/hooks/use-d3-demo.ts [38:244]


export default function useD3Demo({
  ref,
  width,
  height = width,
  minDigit = 0,
  maxDigit = 9,
}: Readonly<Props>) {
  const digitColor = useMemo(
    () =>
      d3
        .scaleOrdinal(d3.schemeTableau10)
        .domain(
          d3.range(minDigit, maxDigit).map((v: number): string => String(v))
        ),
    [maxDigit, minDigit]
  );
  const [linkNum, setLinkNum] = useState<number[]>(
    Array.from({ length: maxDigit - minDigit + 1 }, () => 0)
  );
  const [prevDigit, setPrevDigit] = useState<number>();
  const [innerRadius, setInnerRadius] = useState(width * 0.4);
  const [currentWidth, setCurrentWidth] = useState(width);
  const [currentHeight, setCurrentHeight] = useState(height);

  useEffect(() => {
    if (!ref.current) return;

    ref.current.innerHTML = "";

    const svg = d3
      .select(ref.current)
      .append("svg")
      .attr("preserveAspectRatio", "xMidYMid meet")
      .attr(
        "viewBox",
        `-${currentWidth / 2} -${
          currentHeight / 2
        } ${currentWidth} ${currentHeight}`
      );
    const g = svg.append("g");

    const arcData: ArcData[] = [];
    for (let i = minDigit; i <= maxDigit; i++) {
      arcData.push({
        value: String(i),
        startAngle: Math.PI * 0.2 * i,
        endAngle: Math.PI * 0.2 * (i + 1),
      });
    }

    g.append("defs").attr("id", "transition-defs");

    const outerRadius = innerRadius * 1.1;

    const digitArc = g
      .selectAll(".digitArc")
      .data(arcData)
      .enter()
      .append("g")
      .attr("class", "digitArc");

    const arc = d3
      .arc<ArcData>()
      .innerRadius(innerRadius)
      .outerRadius(outerRadius);

    digitArc
      .append("path")
      .attr("id", (_, i) => "digitArc" + i)
      .attr("d", arc)
      .style("fill", (_, i): string => digitColor(String(i)))
      .style("stroke", (_, i): string => digitColor(String(i)));

    const fontSize = (outerRadius - innerRadius) * 0.8;
    const digitText = digitArc
      .append("text")
      .attr("x", 4)
      .attr("dy", fontSize)
      .attr("font-size", fontSize);

    digitText
      .append("textPath")
      .attr("xlink:href", (d, i) => "#digitArc" + i)
      .text((d) => d.value);

    g.append("g").attr("class", "transition").attr("fill", "none");
  }, [
    currentHeight,
    currentWidth,
    digitColor,
    innerRadius,
    maxDigit,
    minDigit,
    ref,
  ]);

  const init = useCallback(() => {
    setLinkNum(Array.from({ length: maxDigit - minDigit + 1 }, () => 0));
    setPrevDigit(undefined);
    setCurrentWidth(width);
    setCurrentHeight(height);
    setInnerRadius(width * 0.4);
    d3.select(ref.current).selectAll("svg g.transition path").remove();
  }, [height, maxDigit, minDigit, ref, width]);

  const drawLine = useCallback(
    (fromDigit: number, toDigit: number) => {
      const svg = d3.select(ref.current);

      const line = d3
        .line()
        .x((d) => d[0])
        .y((d) => d[1])
        .curve(d3.curveBasis);

      const fromAngle = Math.PI * 0.2 * fromDigit + linkNum[fromDigit] * 0.0025;

      // If the angle has rolled over into the angle for the next digit
      // we need to reset the angle so we stay within the angle range for
      // fromDigit. Here we reset the linkNum so that we start over from the
      // original angle.
      if (fromAngle >= Math.PI * 0.2 * (fromDigit + 1)) {
        setLinkNum((l) => {
          l[fromDigit] = 0;
          return l;
        });
      }

      const toAngle = Math.PI * 0.2 * toDigit + linkNum[toDigit] * 0.0025;
      if (toAngle >= Math.PI * 0.2 * (toDigit + 1)) {
        setLinkNum((l) => {
          l[toDigit] = 0;
          return l;
        });
      }

      const fromPoint: [number, number] = [
        innerRadius * Math.sin(fromAngle),
        innerRadius * Math.cos(fromAngle) * -1,
      ];

      const middlePoint: [number, number] = [
        // TODO: get true halfway point between to radian values
        0, 0,
      ];

      const toPoint: [number, number] = [
        innerRadius * Math.sin(toAngle),
        innerRadius * Math.cos(toAngle) * -1,
      ];

      const gradId = getGradId(fromDigit, toDigit);

      if (!svg.select(gradId).node()) {
        svg
          .select("#transition-defs")
          .append("linearGradient")
          .attr("id", gradId)
          .attr("gradientUnits", "userSpaceOnUse")
          .attr("x1", fromPoint[0])
          .attr("y1", fromPoint[1])
          .attr("x2", toPoint[0])
          .attr("y2", toPoint[1])
          .selectAll("stop")
          .data([fromDigit, toDigit])
          .enter()
          .append("stop")
          .attr("offset", (_, i) => i * 100 + "%")
          .attr("stop-color", (d) => d3.rgb(digitColor(String(d))).formatHex());
      }

      // finally add the line path element
      const transitions = svg.select(".transition");
      transitions
        .append("path")
        .attr("d", line([fromPoint, middlePoint, toPoint]))
        .attr("stroke", () => "url(#" + getGradId(fromDigit, toDigit) + ")")
        .style("stroke-opacity", 0.75);

      setLinkNum((l) => {
        l[fromDigit]++;
        l[toDigit]++;
        return l;
      });
    },
    [digitColor, innerRadius, linkNum, ref]
  );

  const draw = useCallback(
    (d: number) => {
      if (prevDigit) {
        drawLine(prevDigit, d);
      }
      setPrevDigit(d);
    },
    [prevDigit, drawLine]
  );

  /*
  useEffect(() => {
    const svg = d3.select(ref.current).select("svg");
    svg.attr("width", width).attr("height", height);
    svg.select("g").attr("transform", `translate(${width / 2}, ${height / 2})`);
  }, [width, height, ref]);
*/
  return { init, draw };
}