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

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

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

问题分析

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

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

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

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

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

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

ViiTor实时翻译 ViiTor实时翻译

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

ViiTor实时翻译 116 查看详情 ViiTor实时翻译

将缩放函数绑定到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('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:实现整体拖拽功能的解决方案的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/211892.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月3日 13:06:33
下一篇 2025年11月3日 13:08:32

相关推荐

  • 在Laravel框架中如何解决“Too many open files”错误?

    在laravel框架中解决“too many open files”错误的方法 在使用php7.3和laravel框架执行定时任务时,你可能会遇到一个错误提示,指出“打开文件太多”,错误信息大致如下: [2023-03-15 00:14:13] local.ERROR: include(/www/v…

    好文分享 2025年12月11日
    000
  • php中的卷曲:如何在REST API中使用PHP卷曲扩展

    php客户端url(curl)扩展是开发人员的强大工具,可以与远程服务器和rest api无缝交互。通过利用libcurl(备受尊敬的多协议文件传输库),php curl有助于有效执行各种网络协议,包括http,https和ftp。该扩展名提供了对http请求的颗粒状控制,支持多个并发操作,并提供内…

    2025年12月11日
    000
  • 如何用PHP和CURL高效采集新闻列表及详情?

    本文将阐述如何利用PHP和cURL高效抓取目标网站的新闻列表和新闻详情,并展示最终结果。 关键在于高效运用cURL获取数据,处理相对路径并提取所需信息。 首先,解决第一个挑战:从列表页(例如,页面1)提取新闻标题和完整URL。 代码示例如下: <?php$url = 'http://…

    2025年12月11日
    000
  • HTML表单onsubmit事件失效,如何排查表单验证问题?

    HTML表单提交验证失效:排查与解决 在使用HTML表单进行数据提交时,onsubmit事件常用于客户端验证,确保数据符合要求后再提交至服务器。然而,onsubmit事件有时失效,导致表单直接提交,本文将分析一个案例,解决onsubmit=”return check()”失效的问题。 问题描述: 用…

    2025年12月11日
    000
  • Beego项目中如何访问main函数定义的全局变量?

    在Beego项目中,如何正确访问main函数中定义的全局变量?本文将详细讲解如何在Go语言的Beego框架中,从非main.go文件(例如controllers目录下的文件)访问在main.go文件中定义的全局变量。对于Go语言新手来说,这个问题常常令人困惑。 问题背景:假设您需要在一个Beego项…

    2025年12月11日
    000
  • PHP二维数组如何排序并添加排名?

    PHP二维数组排序及排名:高效解决方案 本文将详细阐述如何对PHP二维数组进行排序,并为每个子数组添加排名信息。假设我们的二维数组包含多个子数组,每个子数组包含“xuhao”(序号)和“piaoshu”(票数)两个字段。目标是根据“piaoshu”字段降序排序,票数相同时则按“xuhao”字段升序排…

    2025年12月11日
    000
  • HTML表单onsubmit事件无效,表单仍提交:问题出在哪里?

    HTML表单onsubmit事件失效:排查与解决 在使用HTML表单时,onsubmit事件通常用于表单提交前的验证。然而,有时即使添加了onsubmit=”return check();”,表单仍会直接提交。本文分析此问题,并提供解决方案。 问题描述: 用户在HTML表单中添加onsubmit=”…

    2025年12月11日
    000
  • ThinkPHP5框架下如何不修改模型实现Archives表与B表的多表关联查询?

    ThinkPHP5框架多表关联查询:无需修改模型 本文介绍如何在ThinkPHP5框架中,不修改现有模型的情况下,实现Archives表与自定义表B的多表关联查询,并以Archives表数据为主返回结果。 此方法适用于已有的TP5 CMS系统,需要在原有Archives模型查询基础上关联其他表的情况…

    2025年12月11日
    000
  • 高效的异步操作:Guzzle Promises 的实践与应用

    最近在开发一个需要同时访问多个外部 API 的应用时,遇到了严重的性能问题。 传统的同步请求方式导致应用响应时间过长,用户体验极差。 每个 API 请求都需要等待完成才能发出下一个请求,这在处理大量请求时效率极低,严重影响了系统的吞吐量。 为了解决这个问题,我开始寻找异步处理的方案,最终选择了 Gu…

    2025年12月11日
    000
  • PHP记录:PHP日志分析的最佳实践

    php日志记录对于监视和调试web应用程序以及捕获关键事件,错误和运行时行为至关重要。它为系统性能提供了宝贵的见解,有助于识别问题,并支持更快的故障排除和决策 – 但仅当它有效地实施时。 在此博客中,我概述了PHP记录以及它在Web应用程序中的使用方式。然后,我概述了一些关键的最佳实践,…

    2025年12月11日
    000
  • 告别依赖注入的困扰:使用 PSR-11 容器接口简化代码

    我最近参与了一个大型PHP项目的重构工作。项目中充斥着大量的new操作,各个类之间紧密耦合,代码难以测试和维护。修改一个类往往需要修改多个地方,这使得开发效率极低,而且容易引入新的bug。 我意识到,我们需要引入依赖注入来改善这种情况。然而,仅仅引入依赖注入的概念还不够,我们需要一个高效的机制来管理…

    2025年12月11日
    000
  • 高效处理 JSON 数据:scienta/doctrine-json-functions 库的使用指南

    我最近参与的项目使用了 Doctrine ORM 管理数据库,其中一个实体包含一个 JSON 类型的字段,用于存储用户的配置信息。最初,我尝试使用原生 SQL 查询来处理 JSON 数据,例如使用 MySQL 的 JSON_EXTRACT 函数。这种方法虽然可以实现功能,但代码变得冗长且难以阅读,而…

    2025年12月11日
    000
  • 告别崩溃:使用Sentry提升Symfony应用的稳定性

    在开发过程中,我们都经历过应用崩溃的痛苦。 用户报告问题,但我们却苦于无法快速定位错误,只能在茫茫代码海洋中大海捞针。 更糟糕的是,一些错误可能只在特定环境或用户操作下才会出现,难以在本地复现。 我之前的项目使用的是简单的日志记录,虽然能记录一些错误信息,但缺乏上下文信息,例如请求参数、用户身份、堆…

    2025年12月11日
    000
  • 告别数据库操作难题:CakePHP Datasource 库的实践指南

    在之前的项目中,我使用的是传统的数据库连接和操作方式,例如直接使用PDO或数据库驱动程序。随着项目规模的扩大和数据源类型的增加,这种方法的缺点逐渐显现出来: 代码冗余: 对于不同的数据库操作(查询、保存、删除等),以及不同的数据源,都需要编写大量的重复代码。难以维护: 代码难以理解和维护,修改一个地…

    2025年12月11日
    000
  • 如何高效查询MySQL中指定部门及其所有子部门下的所有员工?

    高效查询mysql中指定部门及其所有子部门下的所有员工 本文介绍如何高效查询MySQL数据库中指定部门(包含所有子部门)下的所有员工信息,并处理员工可能隶属于多个部门的情况。 数据库包含三个表:department(部门表)、user(员工表)和department_user_relate(部门员工…

    2025年12月11日
    000
  • Composer安装RabbitMQ扩展时如何解决版本冲突问题?

    Composer安装php-amqplib扩展时解决版本冲突 在使用Composer安装php-amqplib/php-amqplib扩展时,常常会遇到版本冲突问题。例如,项目可能声明了alibabacloud/darabonba-openapi的版本约束为^2.1,而php-amqplib依赖的库…

    2025年12月11日
    000
  • 告别异步操作的噩梦:Guzzle Promises 的高效应用

    最近我负责一个项目,需要从多个远程服务器上获取数据。传统的做法是使用嵌套的回调函数,代码变得难以维护和理解,而且随着服务器数量的增加,代码复杂度呈指数级增长。 更糟糕的是,这种方法难以处理错误,调试起来也异常困难。 我的代码看起来像一团乱麻,充满了then()和catch(),简直是异步操作的噩梦!…

    2025年12月11日
    000
  • 高效利用多核CPU:Fidry/cpu-core-counter 库的实践指南

    最近在开发一个需要进行大量并行计算的PHP应用时,遇到了一个难题:如何准确地获取系统CPU的核心数,以便合理地分配任务,充分利用多核处理器的优势。如果核心数估计过低,则会造成资源浪费;如果估计过高,则可能导致系统负载过重,影响程序稳定性。 起初,我尝试使用一些系统命令来获取核心数,但这些方法的兼容性…

    2025年12月11日
    000
  • Docker中apt-get update失败:如何正确配置阿里云镜像源?

    Docker中apt-get update失败:阿里云镜像源配置详解 许多开发者在使用Docker构建基于Debian系统的镜像时,会遇到apt-get update命令执行失败的问题。本文以php:5.6-fpm镜像为例,详细说明如何正确配置阿里云镜像源,解决apt-get update错误。 问…

    2025年12月11日
    000
  • 高效测试异常:Codeception AssertThrows 的救星

    在最近的项目中,我负责编写一个用户管理模块的单元测试。该模块包含一个用户控制器,负责处理用户数据的增删改查。其中,show() 方法用于显示指定 ID 的用户信息。如果用户 ID 不存在,该方法应该抛出一个 NotFoundException 异常。 最初,我的测试代码是这样的: class Use…

    2025年12月11日
    000

发表回复

登录后才能评论
关注微信