首页 > web前端 > js教程 > 正文

D3.js Force Directed Graph:实现整体拖拽功能的解决方案

霞舞
发布: 2025-09-01 17:16:01
原创
483人浏览过

d3.js force directed graph:实现整体拖拽功能的解决方案

本文旨在解决D3.js力导向图中无法拖拽整个图的问题。通过将拖拽功能替换为缩放功能,并禁用鼠标滚轮缩放,实现了对整个图的平移操作,同时保留了节点拖拽的功能。本文将提供详细的代码示例和实现步骤,帮助开发者在D3.js力导向图中实现类似效果。

问题分析

在使用D3.js构建力导向图时,经常需要实现缩放和平移功能。D3.js提供了d3.zoom()来实现缩放功能,但如果直接将拖拽功能应用于包含所有节点和连接线的<g>元素,可能无法达到预期效果,即无法拖动整个图。

解决方案:利用缩放功能实现平移

D3.js的缩放功能实际上是通过改变元素的transform属性来实现的,这为我们提供了一个思路:可以使用缩放功能来实现平移效果。具体步骤如下:

  1. 创建新的缩放函数: 创建一个新的d3.zoom()函数,并将其绑定到SVG元素上。在这个缩放函数中,我们将修改包含所有节点和连接线的<g>元素的transform属性。

    const zoomSvg = d3.zoom().on('zoom', (event) => {
      group.attr('transform', event.transform).on('wheel.zoom', null);
    });
    登录后复制

    这里,group是包含所有节点和连接线的<g>元素。event.transform包含了缩放和平移的信息。.on('wheel.zoom', null)用于禁用鼠标滚轮缩放,防止与平移操作冲突。

    ViiTor实时翻译
    ViiTor实时翻译

    AI实时多语言翻译专家!强大的语音识别、AR翻译功能。

    ViiTor实时翻译116
    查看详情 ViiTor实时翻译
  2. 将缩放函数绑定到SVG元素: 将新创建的缩放函数绑定到SVG元素上。

    const svg = d3
      .select(container)
      .append('svg')
      .attr('viewBox', [-width / 2, -height / 2, width, height])
      .call(zoomSvg as any);
    登录后复制

    container是包含SVG元素的容器。.call(zoomSvg as any)将缩放函数绑定到SVG元素上。

完整代码示例

下面是完整的代码示例,展示了如何使用缩放功能实现力导向图的整体拖拽功能:

  const data = jsonFyStory(selectedVariable, stories)
  const links = data.links.map((d) => d)
  const nodes = data.nodes.map((d: any) => d)
  const containerRect = container.getBoundingClientRect()
  const height = containerRect.height
  const width = containerRect.width

  function dragstarted() {
    // @ts-ignore
    d3.select(this).classed('fixing', true)
    setDisplayCta(false)
    setDisplayNodeDescription(false)
    setNodeData({})
  }

  function dragged(event: DragEvent, d: any) {
    d.fx = event.x
    d.fy = event.y
    simulation.alpha(1).restart()
    setDisplayNodeDescription(true)
    d.class === 'story-node' && setDisplayCta(true)
    setNodeData({
      name: d.name as string,
      class: d.class as string,
      definition: d.definition as string,
      summary: d.summary as string,
    })
  }

  //   dragended function in case we move away from sticky dragging!
  function dragended(event: DragEvent, d: DNode) {
    // @ts-ignore
    d3.select(this).classed('fixed', true)
    console.log(d)
  }

  function click(event: TouchEvent, d: DNode) {
    delete d.fx
    delete d.fy
    console.log(d)
    // @ts-ignore
    d3.select(this).classed('fixed', false)
    // @ts-ignore
    d3.select(this).classed('fixing', false)
    simulation.alpha(1).restart()
  }

  const simulation = d3
    .forceSimulation(nodes as any[])
    .force(
      'link',
      d3.forceLink(links).id((d: any) => d.id)
    )
    .force('charge', d3.forceManyBody().strength(isMobile ? -600 : -1300))
    .force('collision', d3.forceCollide().radius(isMobile ? 5 : 20))
    .force('x', d3.forceX())
    .force('y', d3.forceY())

  if (container.children) {
    d3.select(container).selectAll('*').remove()
  }

  const zoom = d3
    .zoom()
    .on('zoom', (event) => {
      group.attr('transform', event.transform)
    })
    .scaleExtent([0.2, 100])

  const zoomSvg = d3.zoom().on('zoom', (event) => {
    group.attr('transform', event.transform).on('wheel.zoom', null)
  })

  const svg = d3
    .select(container)
    .append('svg')
    .attr('viewBox', [-width / 2, -height / 2, width, height])
    .call(zoomSvg as any)

  const group = svg
    .append('g')
    .attr('width', '100%')
    .attr('height', '100%')
    .call(
      d3
        .drag()
        .on('start', dragstarted)
        .on('drag', dragged as any)
        .on('end', dragended as any) as any
    )
  // .call(zoom as any)

  const link = group
    .append('g')
    .attr('stroke', '#1e1e1e')
    .attr('stroke-opacity', 0.2)
    .selectAll('line')
    .data(links)
    .join('line')

  const node = group
    .append('g')
    .selectAll<SVGCircleElement, { x: number; y: number }>('g')
    .data(nodes)
    .join('g')
    .classed('node', true)
    .classed('fixed', (d: any) => d.fx !== undefined)
    .attr('class', (d: any) => d.class as string)
    .call(
      d3
        .drag()
        .on('start', dragstarted)
        .on('drag', dragged as any)
        .on('end', dragended as any) as any
    )
    .on('click', click as any)

  d3.selectAll('.category-node')
    .append('circle')
    .attr('fill', '#0083C5')
    .attr('r', isMobile ? 4 : 7)

  d3.selectAll('.tag-node')
    .append('circle')
    .attr('fill', '#FFC434')
    .attr('r', isMobile ? 4 : 7)

  d3.selectAll('.story-node')
    .append('foreignObject')
    .attr('height', isMobile ? 18 : 35)
    .attr('width', isMobile ? 18 : 35)
    .attr('x', isMobile ? -9 : -17)
    .attr('y', isMobile ? -18 : -30)
    .attr('r', isMobile ? 16 : 30)
    .append('xhtml:div')
    .attr('class', 'node-image')
    .append('xhtml:img')
    .attr('src', (d: any) => d.image)
    .attr('transform-origin', 'center')
    .attr('height', isMobile ? 18 : 35)
    .attr('width', isMobile ? 18 : 35)

  d3.selectAll('.main-story-node')
    .append('foreignObject')
    .attr('height', isMobile ? 50 : 100)
    .attr('width', isMobile ? 50 : 100)
    .attr('x', isMobile ? -25 : -50)
    .attr('y', isMobile ? -25 : -50)
    .attr('r', isMobile ? 50 : 100)
    .append('xhtml:div')
    .attr('class', 'node-image')
    .append('xhtml:img')
    .attr('src', (d: any) => d.image)
    .attr('transform-origin', 'center')
    .attr('height', isMobile ? 50 : 100)
    .attr('width', isMobile ? 50 : 100)

  node
    .append('foreignObject')
    .attr('height', (d: any) => (d.class === 'main-story-node' ? 65 : 55))
    .attr('width', (d: any) =>
      isMobile
        ? d.class === 'main-story-node'
          ? 80
          : 50
        : d.class === 'main-story-node'
        ? 120
        : 70
    )
    .attr('x', (d: any) =>
      isMobile
        ? d.class === 'main-story-node'
          ? -40
          : -25
        : d.class === 'main-story-node'
        ? -60
        : -35
    )
    .attr('y', (d: any) =>
      isMobile
        ? d.class === 'main-story-node'
          ? 32
          : 7
        : d.class === 'main-story-node'
        ? 60
        : 12
    )
    .append('xhtml:p')
    .attr('class', (d: any) => d.class)
    .text((d: any) => d.name)

  simulation.on('tick', () => {
    link
      .attr('x1', (d: any) => d.source.x)
      .attr('y1', (d: any) => d.source.y)
      .attr('x2', (d: any) => d.target.x)
      .attr('y2', (d: any) => d.target.y)
    node
      .attr('cx', (d: any) => d.x as number)
      .attr('cy', (d: any) => d.y as number)
      .attr('transform', (d: any) => {
        return `translate(${d.x},${d.y})`
      })
  })

  function transition(zoomLevel: number) {
    group
      .transition()
      .delay(100)
      .duration(500)
      .call(zoom.scaleBy as any, zoomLevel)
  }

  transition(0.7)

  d3.selectAll('.zoom-button').on('click', function () {
    // @ts-ignore
    if (this && this.id === 'zoom-in') {
      transition(1.2) // increase on 0.2 each time
    }
    // @ts-ignore
    if (this.id === 'zoom-out') {
      transition(0.8) // deacrease on 0.2 each time
    }
    // @ts-ignore
    if (this.id === 'zoom-init') {
      group
        .transition()
        .delay(100)
        .duration(500)
        .call(zoom.scaleTo as any, 0.7) // return to initial state
    }
  })
登录后复制

注意事项

  • 禁用鼠标滚轮缩放: 为了避免鼠标滚轮缩放与平移操作冲突,建议禁用鼠标滚轮缩放功能。
  • 性能优化: 对于大型力导向图,频繁的transform属性更新可能会影响性能。可以考虑使用requestAnimationFrame来优化性能。
  • 兼容性: 该解决方案基于D3.js的缩放功能,请确保你的D3.js版本支持该功能。

总结

通过利用D3.js的缩放功能,我们可以轻松实现力导向图的整体拖拽功能,同时保留节点拖拽的功能。这种方法简单有效,可以满足大多数应用场景的需求。在实际应用中,可以根据具体需求进行调整和优化,例如添加过渡效果、调整缩放比例等。

以上就是D3.js Force Directed Graph:实现整体拖拽功能的解决方案的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号