/ src / components / DisplayContent.jsx
DisplayContent.jsx
  1  import React, { useEffect, useRef } from 'react';
  2  import * as d3 from 'd3';
  3  
  4  const DisplayContent = ({ data, onCircleClick }) => {
  5    const svgRef = useRef(null);
  6  
  7    useEffect(() => {
  8      if (!data || !svgRef.current) return;
  9  
 10      const width = 928;
 11      const height = width;
 12  
 13      const color = d3.scaleLinear()
 14        .domain([0, 5])
 15        .range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
 16        .interpolate(d3.interpolateHcl);
 17  
 18      const pack = data => d3.pack()
 19        .size([width, height])
 20        .padding(3)
 21        (d3.hierarchy(data)
 22          .sum(d => d.value)
 23          .sort((a, b) => b.value - a.value));
 24  
 25      const root = pack(data);
 26  
 27      const svg = d3.select(svgRef.current)
 28        .attr("viewBox", `-${width / 2} -${height / 2} ${width} ${height}`)
 29        .attr("width", width)
 30        .attr("height", height)
 31        .attr("style", `max-width: 100%; height: auto; display: block; margin: 0 -14px; background: ${color(0)}; cursor: pointer;`);
 32  
 33      svg.selectAll("*").remove(); // Clear previous content
 34  
 35      const node = svg.append("g")
 36        .selectAll("circle")
 37        .data(root.descendants().slice(1))
 38        .join("circle")
 39          .attr("fill", d => d.children ? color(d.depth) : "white")
 40          .attr("pointer-events", d => !d.children ? "none" : null)
 41          .on("mouseover", function() { d3.select(this).attr("stroke", "#000"); })
 42          .on("mouseout", function() { d3.select(this).attr("stroke", null); })
 43          .on("click", (event, d) => {
 44            if (focus !== d) {
 45              zoom(event, d);
 46              event.stopPropagation();
 47            }
 48            if (!d.children) {
 49              onCircleClick(d.data.name);
 50            }
 51          });
 52  
 53      const label = svg.append("g")
 54        .style("font", "10px sans-serif")
 55        .attr("pointer-events", "none")
 56        .attr("text-anchor", "middle")
 57        .selectAll("text")
 58        .data(root.descendants())
 59        .join("text")
 60          .style("fill-opacity", d => d.parent === root ? 1 : 0)
 61          .style("display", d => d.parent === root ? "inline" : "none")
 62          .text(d => d.data.name);
 63  
 64      svg.on("click", (event) => zoom(event, root));
 65      let focus = root;
 66      let view;
 67      zoomTo([focus.x, focus.y, focus.r * 2]);
 68  
 69      function zoomTo(v) {
 70        const k = width / v[2];
 71  
 72        view = v;
 73  
 74        label.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`);
 75        node.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`);
 76        node.attr("r", d => d.r * k);
 77      }
 78  
 79      function zoom(event, d) {
 80        const focus0 = focus;
 81  
 82        focus = d;
 83  
 84        const transition = svg.transition()
 85          .duration(event.altKey ? 7500 : 750)
 86          .tween("zoom", d => {
 87            const i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2]);
 88            return t => zoomTo(i(t));
 89          });
 90  
 91        label
 92          .filter(function(d) { return d.parent === focus || this.style.display === "inline"; })
 93          .transition(transition)
 94            .style("fill-opacity", d => d.parent === focus ? 1 : 0)
 95            .on("start", function(d) { if (d.parent === focus) this.style.display = "inline"; })
 96            .on("end", function(d) { if (d.parent !== focus) this.style.display = "none"; });
 97      }
 98    }, [data, onCircleClick]);
 99  
100    return <svg ref={svgRef}></svg>;
101  };
102  
103  export default DisplayContent;