0

I'm trying to get a react component working which uses the d3 sunburst chart. The problem I'm facing is I need a way to update the zoom level of the sunburst component, as a trigger from an external component. I'm sending the node to be zoomed to via the props to the sunburst component, and that changes each time there is an external input for a different component.

Here is the pseudocode I have so far but, each the the props changes.

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

const SunburstSmooth = (props) => {
  const prevProps = usePrevious(props);

  useEffect(() => {
    if (!isEqual(prevProps, props)) {
      if (props.navigateTo) {
        zoomToSunburst(props.navigateTo);
      } else {
        if (props.data) {
          renderSunburstSmooth();
          update();
        }

      }
    }
  }, [props])

  // Global Variables
  let root, node;
  let gWidth, gHeight, radius, svg;
  let color;
  let x, y, arc, partition;

  const svgRef = useRef();

const zoomToSunburst = (nodeToRender) => {
    const gWidth = props.width;
    const gHeight = props.height;
    const radius = (Math.min(gWidth, gHeight) / 2) - 10
    const svg = d3.select(svgRef.current)
    const x = d3.scaleLinear().range([0, 2 * Math.PI])
    const y = d3.scaleSqrt().range([0, radius])
    const partition = d3.partition()
    const arc = d3.arc()
      // ....

    root = d3.hierarchy(nodeToRender);
    node = nodeToRender;
    svg.selectAll("path")
      .transition("update")
      .duration(1000)
      .attrTween("d", (d, i) =>
        arcTweenPath(d, i, radius, x, y, arc));
}


const update = () => {
    root.sum(d => d[props.value]);

    let gSlices = svg.selectAll("g")
      .data(partition(root).descendants())
      .enter()
      .append("g");
    gSlices.exit().remove();
    gSlices.append("path")
      .style('fill', (d) => {
        let hue;
        const current = d;

        if (current.depth === 0) {
          return '#c6bebe';
        }

        return color((current.children ? current.x0 : current.parent.x0));
      })     
      .attr('stroke', '#fff')
      .attr('stroke-width', '1')
 

    svg.selectAll("path")
      .transition("update")
      .duration(750)
      .attrTween("d", (d, i) =>
        arcTweenPath(d, i, radius, x, y, arc));
  }

// Sets up the initial sunburst
const renderSunburstSmooth = () => {
   // Structure
    gWidth = props.width;
    gHeight = props.height;
    radius = (Math.min(gWidth, gHeight) / 2) - 10;

    // Size our <svg> element, add a <g> element, and move translate 0,0 to the center of the element.
    svg = d3.select(svgRef.current)
      .append("g")
      .attr("id", "bigG")
      .attr("transform", `translate(${gWidth / 2},${gHeight / 2})`);

    x = d3.scaleLinear().range([0, 2 * Math.PI]);
    y = d3.scaleSqrt().range([0, radius]);

    // Calculate the d path for each slice.
    arc = d3.arc()
       // .....   

    // Create our sunburst data structure
    partition = d3.partition();

    // Root Data
    root = d3.hierarchy(props.data);
    node = props.navigateTo || root;
}


return (
    <div id={props.keyId}>
      <svg ref={svgRef}/>
    </div>
  );
}

A lot of the code base code is from here: http://bl.ocks.org/metmajer/5480307

Right now each time the prop is updated, the entire component is re-rendered. How do i make it so that it only updates the existing svg container, when the props.navigateTo is changed externally.

falc0nit3
  • 999
  • 2
  • 8
  • 16
  • Did you get this working? And link to demo or source which I can refer? Struggling with getting d3 sunburst working with react with custom tooltip on hover and click to zoom. – Jay May 30 '22 at 23:41

1 Answers1

0

Right now, the rendering of your component depends on a change in any element in your props. In order to make it only depend on the change of the prop navigateTo, you would need two things: 1- navigateTo would need to be a state created with something like const [navigateTo, setNavigateTo] = UseState(""); in the parent component, and passed down as a prop. (I'm just putting this here to make sure you are doing this) So something like:

const parent = (props) => {
    const [navigateTo, setNavigateTo] = UseState("");
    <<any other code>>
    return <SunburstSmooth
            navigateTo={navigateTo}
            data={data}
           >
}

2- To make your code clearer, you can decompose the props to make the rendering depending on only a certain element of it:

const SunburstSmooth = (props) => {
  const {navigateTo, data, ...rest} = props;

  useEffect(() => {
    if (data) {
       renderSunburstSmooth();
       update();
    }
  }, [navigateTo])
  <<rest of your code>>

This ensures that the component is re-rendered only on changes of navigateTo, and not when something like data or any other prop is changed. In case you also want it to re-render every time data is changed, for example, you can just add it to the array at the end of the UseEffect hook

useEffect(() => {...}, [navigateTo, data])

Regarding the re-rendering of only the SVG element, any useEffect hook will cause everything in your return to be re-rendered, so the SVG would have to be the only thing your component returns in order to only re-render that. I can't see why you would mind re-rendering the enclosing div though