Home > OS >  How can I expand child nodes when a node is clicked in d3?
How can I expand child nodes when a node is clicked in d3?

Time:01-13

I am new to d3 and I'm a trying to make a visualization with interactive nodes where each node can be clicked. When the node is clicked it should expand to show child nodes. I was able to get all the nodes to display interactively and I added an on click event, but I am not sure how I can get the child nodes to expand on click.

I am using the data from data.children in the onclick function and passing it to d3.hierarchy to set the data as the root. I am just not sure how to expand the data.

I am looking to make something like this where the circle node is in the center and the child nodes expand around it/outwards.


   child   child
     \     /
      node
        |
      child 

Does anyone have any suggestions on how I could achieve this? I found d3.tree in the docs but that is more of a horizontal tree structure.

export default function ThirdTab(): React.MixedElement {
  const ref = useRef();
  const viewportDimension = getViewportDimension();

  useEffect(() => {
    const width = viewportDimension.width - 150;
    const height = viewportDimension.height - 230;

    const svg = d3
      .select(ref.current)
      .style('width', width)
      .style('height', height);

    const zoomG = svg.attr('width', width).attr('height', height).append('g');

    const g = zoomG
      .append('g')
      .attr('transform', `translate(500,280) scale(0.31)`);

    svg.call(
      d3.zoom().on('zoom', () => {
        zoomG.attr('transform', d3.event.transform);
      }),
    );

    const nodes = g.selectAll('g').data(annotationData);

    const group = nodes
      .enter()
      .append('g')
      .attr('cx', width / 2)
      .attr('cy', height / 2)
      .attr('class', 'dotContainer')
      .style('cursor', 'pointer')
      .call(
        d3
          .drag()
          .on('start', function dragStarted(d) {
            if (!d3.event.active) simulation.alphaTarget(0.03).restart();
            d.fx = d.x;
            d.fy = d.y;
          })
          .on('drag', function dragged(d) {
            d.fx = d3.event.x;
            d.fy = d3.event.y;
          })
          .on('end', function dragEnded(d) {
            if (!d3.event.active) simulation.alphaTarget(0.03);
            d.fx = null;
            d.fy = null;
          }),
      );

    const circle = group
      .append('circle')
      .attr('class', 'dot')
      .attr('r', 20)
      .attr('cx', d => d.x)
      .attr('cy', d => d.y)
      .style('fill', '#33adff')
      .style('fill-opacity', 0.3)
      .attr('stroke', '#b3a2c8')
      .style('stroke-width', 4)
      .attr('id', d => d.name)
      .on('click', function click(data) {
        const root = d3.hierarchy(data.children);
        const links = root.links();
        const nodes = root.descendants();

        console.log(nodes);
      });

    const label = group
      .append('text')
      .attr('x', d => d.x)
      .attr('y', d => d.y)
      .text(d => d.name)
      .style('text-anchor', 'middle')
      .style('fill', '#555')
      .style('font-family', 'Arial')
      .style('font-size', 15);

    const simulation = d3
      .forceSimulation()
      .force(
        'center',
        d3
          .forceCenter()
          .x(width / 2)
          .y(height / 2),
      )
      .force('charge', d3.forceManyBody().strength(1))
      .force(
        'collide',
        d3.forceCollide().strength(0.1).radius(170).iterations(1),
      );

    simulation.nodes(annotationData).on('tick', function () {
      circle
        .attr('cx', function (d) {
          return d.x;
        })
        .attr('cy', function (d) {
          return d.y;
        });

      label
        .attr('x', function (d) {
          return d.x;
        })
        .attr('y', function (d) {
          return d.y   40;
        });
    });
  }, [viewportDimension.width, viewportDimension.height]);

  return (
    <div className="third-tab-content">
      <style>{`
      .tooltip {
        position: absolute;
        z-index: 10;
        visibility: hidden;
        background-color: lightblue;
        text-align: center;
        padding: 4px;
        border-radius: 4px;
        font-weight: bold;
        color: rgb(179, 162, 200);
    }
   `}</style>
      <svg
        ref={ref}
        id="annotation-container"
        role="img"
        title="Goal Tree Container"></svg>
    </div>
  );
}

Node image

CodePudding user response:

`useEffect(() => { const width = viewportDimension.width - 150; const height = viewportDimension.height - 230;

const svg = d3
  .select(ref.current)
  .style('width', width)
  .style('height', height);

const zoomG = svg.attr('width', width).attr('height', height).append('g');

const g = zoomG
  .append('g')
  .attr('transform', `translate(500,280) scale(0.31)`);

svg.call(
  d3.zoom().on('zoom', () => {
    zoomG.attr('transform', d3.event.transform);
  }),
);

const nodes = g.selectAll('g').data(annotationData);

const simulation = d3
  .forceSimulation(annotationData)
  .force(
    'center',
    d3
      .forceCenter()
      .x(width / 2)
      .y(height / 2),
  )
  .force('charge', d3.forceManyBody().strength(1));

const group = nodes
  .enter()
  .append('g')
  .attr('x', d => d.x)
  .attr('y', d => d.y)
  .attr('id', d => 'container'   d.index)
  .attr('class', 'dotContainer')
  .style('white-space', 'pre')
  .style('cursor', 'pointer')
  .call(
    d3
      .drag()
      .on('start', function dragStarted(d) {
        if (!d3.event.active) simulation.alphaTarget(0.03).restart();
        d.fx = d.x;
        d.fy = d.y;
      })
      .on('drag', function dragged(d) {
        d.fx = d3.event.x;
        d.fy = d3.event.y;
      })
      .on('end', function dragEnded(d) {
    
        if (!d3.event.active) simulation.alphaTarget(0.03);
        d.fx = null;
        d.fy = null;
      }),
  );

simulation.on('tick', function () {
  group.attr('transform', function (d) {
    return 'translate('   d.x   ','   d.y   ')';
  });
  children
    .attr('x', function (d) {
      return d.x;
    })
    .attr('y', function (d) {
      return d.y;
    });
});

simulation.force(
  'collide',
  d3.forceCollide().strength(0.1).radius(170).iterations(10),
);

let currentlyExpandedNode;
let currentNode;

const circle = group
  .append('circle')
  .attr('class', 'dot')
  .attr('id', d => {
    return 'circle'   d.index;
  })
  .attr('r', 20)
  .attr('cx', d => d.x)
  .attr('cy', d => d.y)
  .style('fill', '#33adff')
  .style('fill-opacity', 0.3)
  .attr('stroke', 'gray')
  .style('stroke-width', 4)
  .on('click', function click(data) {
    currentNode = d3.select(`#container${data.index}`);

    if (currentlyExpandedNode) {
      d3.selectAll('.child').remove();
      d3.selectAll('#child-text').remove();
    }

    currentlyExpandedNode = data;

    const pie = d3
      .pie()
      .value(() => 1)
      .sort(null);

    const circlex1 =  currentNode.select(`#circle${data.index}`).attr('cx');
    const circley1 =  currentNode.select(`#circle${data.index}`).attr('cy');

    const children = currentNode
      .selectAll('line.child')
      .data(pie(data.children))
      .enter()
      .append('line')
      .attr('stroke', 'gray')
      .attr('stroke-width', 1)
      .attr('stroke-dasharray', '5 2')
      .attr('class', 'child')
      .attr('x1', circlex1) // starting point
      .attr('y1', circley1)
      .attr('x2', circlex1) // transition starting point
      .attr('y2', circley1)
      .transition()
      .duration(300)
      .attr('x2', circlex1   62) // end point
      .attr('y2', circley1 - 62);

    const childrenCircles = currentNode
      .selectAll('circle.child')
      .data(data.children)
      .enter()
      .append('circle')
      .attr('class', 'child')
      .attr('cx', () => circlex1   70)
      .attr('cy', () => circley1 - 70)
      .attr('r', 10)
      .style('fill', '#b3a2c8')
      .style('fill-opacity', 0.8)
      .attr('stroke', 'gray')
      .style('stroke-width', 2);

    children.each(childData => {
      currentNode
        .append('text')
        .attr('x', () => circlex1   80)
        .attr('y', () => circley1 - 100)
        .text(childData.data.name)
        .attr('id', 'child-text')
        .style('text-anchor', 'middle')
        .style('fill', '#555')
        .style('font-family', 'Arial')
        .style('font-size', 15);
    });

  });

group
  .append('text')
  .attr('x', d => d.x)
  .attr('y', d => d.y   50)
  .text(d => d.name)
  .style('text-anchor', 'middle')
  .style('fill', '#555')
  .style('font-family', 'Arial')
  .style('font-size', 15);

const children = group.selectAll('.child-element');

}, [viewportDimension.width, viewportDimension.height]);`

  • Related