【 D3.js 高级系列 — 10.0 】 思维导图

本文介绍了一种使用树状图布局来制作思维导图的方法,包括构造思路树状图、制作思维导图的过程以及实现思维导图的开关功能。详细解释了如何在点击节点时更新树状图显示子节点,并提供了实现代码。

思维导图的节点具有层级关系和隶属关系,很像枝叶从树干伸展开来的形状。在前面讲解布局的时候,提到有五个布局是由层级布局扩展来的,其中的树状图(tree layout)和集群图(cluster layout)布局制作出来的图具有“树形”。因此,可以凭借这两种布局来制作思维导图。

1001


1. 构造思路

树状图布局,将一个具有层级关系的对象root转换成节点数组nodes时,情况如下。有一个root对象:

{
	name: "node1",
	children: 
[
			{ name: "node2" },
			{ name: "node3" }
	]
}
经树状图布局转换后,得到的节点数组nodes如下:
[
{
		name: "node1",
		children: 
		[
			{ name: "node2" },
			{ name: "node3" }
		]
	},
	{ name: "node2" },
	{ name: "node3" }
]

下图是上述节点数组的示意图。由于 node1 具有子节点,可作为开关使用,点击 node1 才会展现 node2 和 node3。

1002

问题是:怎样制作一个“开关”,使得点击树状图中的某个节点时,树状图更新并显示出被点击节点的子节点。

我们知道,树状图的层级关系是由每一个对象的children属性决定的(当然,也可以通过tree.children()修改这一点),也就是说,如果某一个节点的children值为空,则再次用布局计算时,其子节点就不会进入节点数组nodes了。例如,将root改为:

{
	name: "node1",
	children: null
}

则得到的节点数组nodes里将没有node2和node3节点。也就是说,“开关”只要将被点击节点的children设置为null即可。但是,由于将来可能还要用到children节点,可设一临时变量_children保存此值,例如:

{
	name: "node1",
	children: null
       _children: 		/* 临时变量 */
       [
			{ name: "node2" },
			{ name: "node3" }
	]

}

树状图布局不会认为_children是保存子节点的变量,只把它看做是一般的变量而保存下来,因此节点数组nodes里只有一个节点。根据上面的思路,写一个开关切换函数如下。

//切换开关,d 为被点击的节点
function toggle(d){

if(d.children){
//如果有子节点
    	d._children = d.children; //将该子节点保存到 _children
     	d.children = null;  //将子节点设置为null
    
}else{
//如果没有子节点
     	d.children = d._children; //从 _children 取回原来的子节点 
     	d._children = null; //将 _children 设置为 null
    }
}

每次开关状态切换时,都要重新调用布局重新计算节点的位置,也就是说,要有一个重绘函数能够处理数据发生更新的情况。这就又要用到【选择集与数据 - 第 5 章】的处理模板,重绘函数的部分代码如下,尤其要注意开关函数是如何被使用的。

//重绘函数
 function redraw(source){

    //重新计算节点和连线
    var nodes = tree.nodes(root);
    var links = tree.links(nodes);

//获取节点的update部分
    var nodeUpdate = svg.selectAll(".node")
                        .data(nodes, function(d){ return d.name; });

    //获取节点的enter部分
    var nodeEnter = nodeUpdate.enter();

//在给enter部分添加新的节点时,添加监听器,应用开关切换函数
nodeEnter.append("g")
     	   .on("click", function(d) { 
toggle(d); 
redraw(d); 
});

/***************
        省略
***************/
}

每一个被新添加的节点,都会响应click事件。当某个节点被点击时,如果它具有子节点,则在开关切换函数的作用下,root对象被修改了,然后调用重绘函数后,新的树状图将被绘制。如此一来,树状图具有开关功能,也就可以当做思维导图使用了。


2. 制作思维导图

首先,要有一个具有层级关系的 JSON 文件,本文使用:learn.json

其次,依次创建树状图布局、对角线生成器等,用于绘制树状图。

然后,实现最关键的重绘函数,函数声明如下:

function redraw(source)

只有一个参数source,这是被点击的节点,如果该节点原来为闭合状态,点击后其子节点将显现,如果原来为打开状态,点击后其子节点将隐藏。函数体的实现,分为四个步骤:

2.1 调用布局,计算节点和连线数组

树状图布局的tree.nodes()返回节点数组,tree.links()返回连线数组。其中,对节点的y坐标重新计算,使其只与节点的深度有关,由于后期绘制节点和连线时要将x和y坐标对调,因此这里重计算的实际上是水平方向的坐标。

//应用布局,计算节点和连线
var nodes = tree.nodes(root);
var links = tree.links(nodes);

//重新计算节点的y坐标
nodes.forEach(function(d) { d.y = d.depth * 180; });

之所以重新计算y坐标,是为了当数据更新(用于点击节点)时,保证树状图的结构不要发生太大的变化,如此看起来比较自然。

2.2 分别处理节点的update、enter、exit三部分

在svg里选择当前所有的节点,使其与节点数组nodes绑定,绑定时要设定一个键函数。键函数里直接返回d.name,当节点数组发生更新时,新节点要与旧节点在名称上相对应。

//获取节点的update部分
var nodeUpdate = svg.selectAll(".node")
               .data(nodes, function(d){ return d.name; });

//获取节点的enter部分
var nodeEnter = nodeUpdate.enter();

//获取节点的exit部分
var nodeExit = nodeUpdate.exit();

先处理enter部分,即添加节点。节点的构成为:分组元素里有一个圆表示节点,还有一个文字元素表示节点的名称。元素结构如下:

本例中,每一个新添加的节点都将缓慢地过渡到自己本身的位置,如此更具有友好性。因此,新节点的初始位置都设定在source节点处,确切的说是重回之前source节点的位置,该坐标是保存在source.x0和source.y0里的。另外,对于每一个新节点,设置的半径为0,设置为完全透明,接下来在处理update部分的时候会将这些新节点过渡到正常状态的。下图展示了处理enter部分和update部分时如何节点的位置时如何确定和过渡的。

1003

处理enter部分的代码如下。
//1. 节点的 Enter 部分的处理办法
var enterNodes = nodeEnter.append("g")
       .attr("class","node")
       .attr("transform", function(d) { 
return "translate(" + source.y0 + "," + source.x0 + ")"; 
})
       .on("click", function(d) { 
toggle(d); 
redraw(d); 
});

//省略添加圆和文字部分

然后处理update部分,将所有节点(包括在enter部分新添加的节点)都缓缓过渡到新的位置。由于新的节点数组是与节点选择集绑定在一起的,因此d.x和d.y里保存的就是新的坐标值。

//2. 节点的 Update 部分的处理办法
var updateNodes = nodeUpdate.transition()
               .duration(500)
               .attr("transform", function(d) { 
return "translate(" + d.y + "," + d.x + ")"; 
});
最后处理exit部分,需要删除的节点的位置缓缓过渡到其父节点处。
//3. 节点的 Exit 部分的处理办法
var exitNodes = nodeExit.transition()
 .duration(500)
.attr("transform", function(d) { 
return "translate(" + source.y + "," + source.x + ")"; 
})
 .remove();

2.3 分别处理连线的update、enter、exit三部分

在svg中选择所有的连线,绑定连线数组links,由此可获得连线的update、enter、exit部分。

//获取连线的update部分
var linkUpdate = svg.selectAll(".link")
         .data(links, function(d){ return d.target.name; });

//获取连线的enter部分
var linkEnter = linkUpdate.enter();

//获取连线的exit部分
var linkExit = linkUpdate.exit();

对于连线的enter部分,是插入路径元素path,路径由对角线生成器获取,对角线的起点和终点坐标都是(source.x0, source.y0)。

对于连线的update部分,将所有的连线的位置(对角线的起点和终点)更新到新的位置,即目前绑定的数组links里保存的位置。

对于连线的exit部分,令其缓缓过渡到当前的source点,再移除。

//1. 连线的 Enter 部分的处理办法
linkEnter.insert("path",".node")
          .attr("class", "link")
          .attr("d", function(d) {
              var o = {x: source.x0, y: source.y0};
              return diagonal({source: o, target: o});
          })
          .transition()
          .duration(500)
          .attr("d", diagonal);

 //2. 连线的 Update 部分的处理办法
 linkUpdate.transition()
        .duration(500)
        .attr("d", diagonal);

 //3. 连线的 Exit 部分的处理办法
 linkExit.transition()
          .duration(500)
          .attr("d", function(d) {
            var o = {x: source.x, y: source.y};
            return diagonal({source: o, target: o});
          })
          .remove();

2.4 保存当前的节点坐标

当用户点击节点后,数据发生更新,即每个节点的坐标要发生更新。但是,在对节点和连线进行过渡操作的时候,需要使用到更新前的数据(source.x0和source.y0)。因此,每一次调用重绘函数,都要将当前节点的位置保存下来。

nodes.forEach(function(d) {
      d.x0 = d.x;
      d.y0 = d.y;
});

x和y坐标分别保存在x0和y0中,在调用redraw(source)时,被点击的节点被作为参数传到了重绘函数里,因此source.x0和source.y0里保存的是被点击之前节点的坐标。

3. 结果

结果如下图所示,点击节点可以展开子节点。

1004 1005

源代码请单击以下链接,邮件查看源代码:

http://www.ourd3js.com/demo/G-10.0/mind.html

谢谢阅读。

文档信息

<think>嗯,用户现在想了解如何使用Node.js开发一个思维导图项目,需要教程或示例代码。首先,我需要回顾之前的对话历史,看看有没有相关的内容。用户之前问过怎么用XMind入职,我回答过关于XMind的使用方法,现在转向技术实现,使用Node.js来开发这样的项目。 根据提供的引用,用户给出了一些参考资料,比如使用JavaScript库如d3.js、mindmap.js,以及Node.js模块化的知识。引用1提到用d3.js思维导图的代码示例,引用2和4涉及Node.js的特性,比如模块化、非阻塞I/O,以及处理路径的问题。引用3提供了使用js实现的demo和源码链接。 用户的需求很明确,是要用Node.js开发思维导图项目。但需要明确的是,Node.js通常用于后端,而思维导图的前端可能需要JavaScript库。因此,可能需要结合Node.js后端和前端的JavaScript库来实现完整的项目。 首先,我应该考虑项目的结构:后端用Node.js处理数据存储和API,前端用JavaScript库如d3.js或mindmap.js渲染思维导图。用户可能需要示例代码,所以需要分步骤说明如何搭建后端,如何实现数据存储,以及如何与前端交互。 接着,根据引用中的信息,引用3的demo和源码可能是一个好的起点,但用户需要Node.js部分。所以需要解释如何将前端和后端结合起来,比如使用Express.js搭建服务器,提供静态文件,处理保存和加载思维导图的请求。 然后,需要考虑数据如何存储。Node.js可以使用文件系统模块来保存JSON数据,或者使用数据库如MongoDB。根据用户的需求,可能简单的文件存储更合适,尤其是示例项目。 还需要处理跨域问题,如果前端和后端分开部署的话,不过如果使用同一个服务器提供静态文件和API,可能不需要处理跨域。另外,引用4中提到路径处理的问题,需要注意在Node.js中正确设置静态资源路径。 用户可能对具体实现步骤感兴趣,比如如何创建Express应用,如何定义API路由,如何与前端交互。需要提供代码示例,比如基本的Express服务器设置,以及前端如何调用API保存和加载数据。 另外,可能需要解释如何集成前端的思维导图库,比如使用d3.js来渲染节点,处理用户交互事件(点击添加子节点,拖拽等)。这部分可能需要一些前端代码示例,结合HTML和JavaScript。 最后,应该考虑项目的优化方向,比如实时协作、数据持久化、导出功能等,但作为基础教程,先实现核心功能比较重要。同时,提供相关的问题,帮助用户深入探索,比如如何实现实时同步,如何优化性能等。 总结下来,回答的结构应该是分步骤的教程,包含前后端的设置、数据存储、API实现、前端渲染,并给出代码示例和相关资源链接。同时,结合之前的引用内容,特别是引用1和3中的示例,以及引用2和4中的Node.js知识,确保回答的准确性和实用性。</think>以下是使用Node.js开发思维导图项目的分步指南,结合前后端实现: --- ### **一、项目架构设计** 1. **技术选型**: - 后端:Node.js + Express(Web框架) - 前端:d3.js/mindmap.js(可视化渲染) - 数据存储:JSON文件/MongoDB ```mermaid graph LR A[前端] -->|HTTP请求| B(Node.js服务) B -->|读写| C[(JSON文件)] ``` --- ### **二、后端实现(Node.js)** #### **1. 初始化项目** ```bash mkdir mindmap-project && cd mindmap-project npm init -y npm install express body-parser cors ``` #### **2. 创建基础服务** ```javascript // server.js const express = require(&#39;express&#39;); const fs = require(&#39;fs&#39;); const app = express(); app.use(express.json()); // 保存思维导图数据 app.post(&#39;/save&#39;, (req, res) => { const data = req.body; fs.writeFileSync(&#39;mindmap.json&#39;, JSON.stringify(data)); res.status(200).send(&#39;Saved!&#39;); }); // 加载数据 app.get(&#39;/load&#39;, (req, res) => { const data = fs.readFileSync(&#39;mindmap.json&#39;); res.json(JSON.parse(data)); }); app.listen(3000, () => { console.log(&#39;Server running on port 3000&#39;); }); ``` --- ### **三、前端实现** #### **1. 基础HTML结构** ```html <!-- public/index.html --> <!DOCTYPE html> <html> <head> <script src="https://d3js.org/d3.v7.min.js"></script> </head> <body> <svg id="mindmap"></svg> <button onclick="addNode()">添加节点</button> <script src="app.js"></script> </body> </html> ``` #### **2. 使用d3.js渲染思维导图** ```javascript // public/app.js let nodes = [{ id: 1, name: "中心主题", x: 400, y: 300 }]; function render() { d3.select("#mindmap") .selectAll("circle") .data(nodes) .join("circle") .attr("cx", d => d.x) .attr("cy", d => d.y) .attr("r", 20); } // 与Node.js服务交互 async function saveData() { await fetch(&#39;http://localhost:3000/save&#39;, { method: &#39;POST&#39;, headers: { &#39;Content-Type&#39;: &#39;application/json&#39; }, body: JSON.stringify(nodes) }); } async function loadData() { const response = await fetch(&#39;http://localhost:3000/load&#39;); nodes = await response.json(); render(); } ``` --- ### **四、关键功能扩展** #### **1. 节点操作逻辑** ```javascript // 添加子节点(示例) function addNode(parentId) { const parent = nodes.find(n => n.id === parentId); const newNode = { id: Date.now(), name: "新节点", x: parent.x + 100, y: parent.y }; nodes.push(newNode); render(); } // 拖拽交互 d3.selectAll("circle").call( d3.drag().on("drag", (event, d) => { d.x = event.x; d.y = event.y; render(); }) ); ``` --- ### **五、部署与优化** 1. **静态资源服务**: ```javascript // server.js 添加 app.use(express.static(&#39;public&#39;)); ``` 2. **跨域处理**: ```javascript const cors = require(&#39;cors&#39;); app.use(cors()); ``` 3. **数据持久化增强**: ```javascript // 使用MongoDB替换文件存储 const mongoose = require(&#39;mongoose&#39;); mongoose.connect(&#39;mongodb://localhost:27017/mindmaps&#39;); const MindMapSchema = new mongoose.Schema({ data: Object }); const MindMap = mongoose.model(&#39;MindMap&#39;, MindMapSchema); ``` --- ### **六、推荐开源项目参考** 1. **mindmaptree**(引用[^3]): - Demo:http://sources.ikeepstudying.com/mindmaptree-master/ - 源码:https://github.com/RockyRen/mindmaptree 2. **基于SVG.js的实现**: ```bash git clone https://github.com/naver/svg.js ``` --- ### **注意事项** 1. 安全防护:对用户输入内容进行XSS过滤 2. 性能优化:使用WebSocket实现实时协作(引用[^4]的I/O模型) 3. 错误处理:添加try-catch包裹文件操作 --- ### **相关问题** 1. 如何实现思维导图节点的折叠/展开功能? 2. Node.js如何支持多人实时协作编辑? 3. 怎样将思维导图导出为图片/PDF?[^1][^3] 4. 如何通过WebSocket同步节点位置变化?[^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值