donut.jsx 3,53 ko
Newer Older
"use client";

import React, {useEffect, useRef} from "react";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import * as d3 from "d3";

const defaultColorScheme = [
  "#1e64af", "#419bd2", "#69beeb", "#d7b428", "#e19196",
  "#a5236e", "#eb1e4b", "#f06937", "#f5dc50", "#2d969b",
  "#335c6c", "#ff7d2d", "#fac846", "#a0c382", "#5f9b8c",
  "#325a69", "#fff5af", "#e1a03c", "#a0282d", "#550a0f"
];

/**
 * @param {object} props
 * @param {[{region: string, occurrences: number}]} props.data
 */
export default function Donut({data}) {
  const ref = useRef();
  const width = 300;
  const height = 300;
  const padding = 20;
  const thicknessOnMouseOver = 10;
  const radius = Math.min(width, height) / 2 - padding;
  const deathAngle = 0.2;

  useEffect(() => {
    if (data === undefined || data.length === 0) {
      return;
    }

    const rawPieData = d3.pie().value((x) => x.occurrences)(data);

    let pieData = [];
    const aggregate = {
      data: {
          region: "aggregate",
        occurrences: 0
      },
      index: rawPieData.length,
      value: 0,
      padAngle: 0,
      startAngle: 0,
      endAngle: Math.PI * 2
    };

    let lastEndAngle = 0;
    for (const d of rawPieData) {
      if (d.endAngle > lastEndAngle) {
        lastEndAngle = d.endAngle;
      }
      // if d angle is less than deathAngle, we don't show it in final donut
      if (d.endAngle - d.startAngle < deathAngle) {
        aggregate.startAngle = lastEndAngle;
        aggregate.endAngle -= d.endAngle - d.startAngle;
        aggregate.value += d.value;
        aggregate.data.occurrences += d.data.occurrences;
      } else {
        pieData.push(d);
      }
    }
    pieData.push(aggregate);

    const arc = d3
      .arc()
      .innerRadius(radius * 0.6)
      .outerRadius(radius)

    const svg = d3.select(ref.current)
      .attr("width", width)
      .attr("height", height)
      .append("g")
      .attr("transform", `translate(${width / 2}, ${height / 2})`);

    svg.selectAll("path")
    .data(pieData)
    .enter()
    .append("path")
    .attr("d", arc)
    .attr("fill", (x, i) => x.data.region === "aggregate" ? "#dddddd" : defaultColorScheme[i % data.length])
    .on("mouseover", function (event, d) {
        const percentage = (d.value / aggregate.value).toFixed(2);
        const text = `${d.data.region}: ${percentage}%`;
        d3.select(this)
            .transition()
            .duration(200)
            .attr("stroke", this.attributes.fill.nodeValue)
            .attr("stroke-width", thicknessOnMouseOver);

        svg.append("text")
            .attr("class", "tooltip")
            .attr("text-anchor", "middle")
            .attr("dy",-radius)
            .text(text)
            .attr("transform", `translate(${width / 2 - 100}, ${height / 2 - 50})`);
        this.parentNode.appendChild(this);
    })
    .on("mouseout", function () {
        d3.select(this)
            .transition()
            .attr("stroke", "none")
            .attr("stroke-width", 0);

        svg.select(".tooltip").remove();
    });

    }, [data]);

  return (
      <div>
          {/*isLoading && (
              <div
                  style={{
                      position: "absolute",
                      top: "50%",
                      left: "50%",
                      transform: "translate(-50%, -50%)",
                  }}
              >
                  <FontAwesomeIcon icon={faSpinner} spin size="3x" />
              </div>
          )*/}
          <svg ref={ref}></svg>
      </div>
  );
}