javascriptsvgd3.jsd3-force-directedsvg-transforms

How to I apply the zoom transform to my d3 force directed graph?


Other similar answers I have found on StackOverflow are old and no longer apply to the latest version.

My zoomed function is being called. The transform parameter contains:

transform parameter

I assume I will need to take the information in transform.transform and apply it to the transform attribute on the svg. The this variable contains a reference to the SVG.

What I am not sure about is exactly how to do that...?

    const width = 1000
    const height = 400

    const node_data = Array.from({ length: 5 }, () => ({
      group: Math.floor(Math.random() * 3),
    }))

    const edge_data = Array.from({ length: 10 }, () => ({
      source: Math.floor(Math.random() * 5),
      target: Math.floor(Math.random() * 5),
      value: Math.floor(Math.random() * 10) + 1,
    }))

    const links = edge_data.map((d) => ({ ...d }))
    const nodes = node_data.map((d, index) => ({ id: index, ...d }))
    const color = d3.scaleOrdinal(d3.schemeCategory10)

    //
    //
    //

    function zoomed(transform) {
      // console.log(`🚀 ~ zoomed ~ this:`, this)
      // console.log(`🚀 ~ zoomed ~ transform:`, transform)
    }

    const svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed))

    const simulation = d3
      .forceSimulation(nodes)
      .force(
        'link',
        d3
          .forceLink(links)
          .id((d) => d.id)
          .distance((d) => 100)
      )
      .force('charge', d3.forceManyBody())
      .force('center', d3.forceCenter(width / 2, height / 2))
      .on('tick', ticked)

    const link = svg
      .append('g')
      .attr('stroke', '#999')
      .attr('stroke-opacity', 0.6)
      .selectAll()
      .data(links)
      .join('line')
      .attr('stroke-width', (d) => Math.sqrt(d.value))

    const node = svg
      .append('g')
      .attr('stroke', '#fff')
      .attr('stroke-width', 1.5)
      .selectAll()
      .data(nodes)
      .join('circle')
      .attr('r', 16)
      .attr('fill', (d) => color(d.group))

    node.append('title').text((d) => `hello ${d.id}`)

    function ticked() {
      link
        .attr('x1', (d) => d.source.x)
        .attr('y1', (d) => d.source.y)
        .attr('x2', (d) => d.target.x)
        .attr('y2', (d) => d.target.y)

      node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
    }

    node.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))

    function dragstarted(event) {
      if (!event.active) simulation.alphaTarget(0.3).restart()
      event.subject.fx = event.subject.x
      event.subject.fy = event.subject.y
    }

    function dragged(event) {
      event.subject.fx = event.x
      event.subject.fy = event.y
    }

    function dragended(event) {
      if (!event.active) simulation.alphaTarget(0)
      event.subject.fx = null
      event.subject.fy = null
    }
    .graph {
      width: 1000px;
      height: 400px;
    }
    <script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
  <svg ref="chart" id="chart" class="graph"></svg>


Solution

  • The most straight-forward implementation is:

    function zoomed(e) {
      zoomG.attr('transform', e.transform); //<-- apply zoom
    }
    
    const svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed));  //<-- add zoom behavior
    const zoomG = svg.append('g'); //<-- wrap drawing in g to apply zoom on
    

    const width = 1000
        const height = 400
    
        const node_data = Array.from({ length: 5 }, () => ({
          group: Math.floor(Math.random() * 3),
        }))
    
        const edge_data = Array.from({ length: 10 }, () => ({
          source: Math.floor(Math.random() * 5),
          target: Math.floor(Math.random() * 5),
          value: Math.floor(Math.random() * 10) + 1,
        }))
    
        const links = edge_data.map((d) => ({ ...d }))
        const nodes = node_data.map((d, index) => ({ id: index, ...d }))
        const color = d3.scaleOrdinal(d3.schemeCategory10)
    
        //
        //
        //
    
        function zoomed(e) {
          zoomG.attr('transform', e.transform); //<-- apply zoome
        }
    
        const svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed));  //<-- add zoom behavior
        const zoomG = svg.append('g'); //<-- wrap drawing in g to apply zoom on
    
        const simulation = d3
          .forceSimulation(nodes)
          .force(
            'link',
            d3
              .forceLink(links)
              .id((d) => d.id)
              .distance((d) => 100)
          )
          .force('charge', d3.forceManyBody())
          .force('center', d3.forceCenter(width / 2, height / 2))
          .on('tick', ticked)
    
        const link = zoomG
          .append('g')
          .attr('stroke', '#999')
          .attr('stroke-opacity', 0.6)
          .selectAll()
          .data(links)
          .join('line')
          .attr('stroke-width', (d) => Math.sqrt(d.value))
    
        const node = zoomG
          .append('g')
          .attr('stroke', '#fff')
          .attr('stroke-width', 1.5)
          .selectAll()
          .data(nodes)
          .join('circle')
          .attr('r', 16)
          .attr('fill', (d) => color(d.group))
    
        node.append('title').text((d) => `hello ${d.id}`)
    
        function ticked() {
          link
            .attr('x1', (d) => d.source.x)
            .attr('y1', (d) => d.source.y)
            .attr('x2', (d) => d.target.x)
            .attr('y2', (d) => d.target.y)
    
          node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
        }
    
        node.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))
    
        function dragstarted(event) {
          if (!event.active) simulation.alphaTarget(0.3).restart()
          event.subject.fx = event.subject.x
          event.subject.fy = event.subject.y
        }
    
        function dragged(event) {
          event.subject.fx = event.x
          event.subject.fy = event.y
        }
    
        function dragended(event) {
          if (!event.active) simulation.alphaTarget(0)
          event.subject.fx = null
          event.subject.fy = null
        }
    .graph {
          width: 1000px;
          height: 400px;
        }
    <script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
      <svg ref="chart" id="chart" class="graph"></svg>