文章目录
- 📚目标效果
- 📚html和css
- 📚js
- 🐇整体框架
- 🐇细说创建部分
📚目标效果
- 力导向关系图
- 人物详情
- 子图高亮
📚html和css
- html放一个
div
框:<div class="network"></div>
。 - css主要完整高亮后的透明度设置以及悬浮提示框的样式设置。
.link.inactive, .linetext.inactive, .node.inactive image, .node.inactive text { opacity: .2; } .tooltip { font-family: "KaiTi", "serif"; font-size: 12px; width: 220px; height: auto; position: absolute; background: #fff; opacity: 0.5; border-radius: 5px; box-shadow: 0 5px 10px rgba(0, 0, 0, .2); } .tooltip .title { color: #fff; padding: 5px; font-size: 14px; background-color: #d2a36c; border-radius: 5px 5px 0 0; } .tooltip .detail-info { width: 100%; border-collapse: collapse; border: 1px solid #d2a36c; } .tooltip .detail-info td { padding: 3px 5px; color: #666; vertical-align: middle; } .tooltip .detail-info tr:nth-of-type(odd) { background: #f9f9f9; } .tooltip .detail-info td.td-label { color: #333; width:60px; } .tooltip .detail-info td a { color: #666 }
📚js
- 外部引入js
<script type = "text/javascript" src = "./assects/js/d3/d3.v4.js"></script> <script type = "text/javascript" src = "./assects/js/jquery/jquery-1.9.1.js"></script>
- 下述为自定义部分⭐️
🐇整体框架
-
分为两部分:
创建
+应用
d3.json("./data/people.json", function(json) { // 创建部分 function GroupExplorer(wrapper,config){...} // 实例应用 new GroupExplorer('.network',{ data:json }); });
-
数据采用外部
json
导入,基本格式适合桑基图的数据格式一样的。nodes
放诗人具体信息,links
放诗人之间的联系。{ "nodes": [ { "name": "元稹", "zihao": "字微之,别字威明", "dynasty": "中唐", "age": "52岁(779年~831年)", "epithet": "与白居易并称“元白”", "deeds": "提倡“新乐府”,倡导“古文运动”", "famous": "《乐府古题序》、《莺莺传》", "image": "元稹.png", "link": "https://so.gushiwen.cn/authorv_201a0677dee4.aspx" }, ... ], "links": [ { "source": 14, "target": 29, "value": "好友" }, ... ] }
🐇细说创建部分
-
准备工作:设置了初始配置,为可视化准备数据,并使用D3.js为绘制网络图设置了带有缩放功能的SVG画布。
- 默认配置:定义了网络图的默认配置,包括初始数据、宽度、高度和节点之间的距离。
- 配置扩展:使用jQuery的
$.extend()
方法获取数据值。 - 数据转换: 遍历连接并检查源和目标是否不是数字(假设它们是节点的名称)。如果是,则根据名称找到相应的节点,并将它们分配为连接的源和目标。
- 初始化: 初始化主要函数的变量,并设置初始高亮节点以及用于存储依赖节点、连接和文本的数组。
- 缩放功能: 定义了一个缩放处理函数(zoomed)来根据缩放事件对可视化进行转换。还设置了一个具有特定缩放范围和缩放事件处理程序的d3缩放行为。
- SVG绘制: 选择
.network
元素,并在其上附加一个SVG画布,设置其宽度和高度,附加缩放行为,并禁用双击缩放。然后附加一个组元素来存储所有的节点、连接和文本。
// 默认配置对象 var defaultConfig = { data:{"nodes":[],"links":[]}, // 初始宽高为视口宽高 width:window.innerWidth, height:window.innerHeight, distance: 96 }; // jQuery库中的 $.extend()方法,获取数据值 $.extend(true,defaultConfig,config); // link里不是用名字匹配而是用数字(也就是对应对象的索引,即节点数组的顺序) // 检查是否存在名字,进行索引转换 defaultConfig.data.links.forEach(function (e) { if(typeof e.source != "number" && typeof e.target != "number"){ var sourceNode = defaultConfig.data.nodes.filter(function (n) { return n.name === e.source; })[0], targetNode = defaultConfig.data.nodes.filter(function (n) { return n.name === e.target; })[0]; e.source = sourceNode; e.target = targetNode; } }); //d3画图start,首先初始化 var _this = this,highlighted = null,dependsNode = [],dependsLinkAndText = []; // 将缩放变换应用到容器元素 this.zoomed = function(){ _this.vis.attr("transform", d3.event.transform); }; // 设置缩放范围和缩放响应函数。 var zoom = d3.zoom() .scaleExtent([0.2,10]) .on("zoom",function(){ _this.zoomed(); }); // 选择body元素,并创建一个SVG画布,并应用缩放功能 this.vis = d3.select(".network").append("svg:svg") .attr("width", defaultConfig.width) .attr("height", defaultConfig.height) // 实现双击还原 .call(zoom).on("dblclick.zoom", null); // 分组元素,用于放置所有的节点、连接线和文字 this.vis = this.vis.append('g').attr('class','all') .attr("width", defaultConfig.width) .attr("height", defaultConfig.height)
-
创建力导向布局和力模型
this.force = d3.forceSimulation() // 指定节点 .nodes(defaultConfig.data.nodes) // 引力 .force("link", d3.forceLink(defaultConfig.data.links).distance(defaultConfig.distance)) .force("linkForce", d3.forceLink().id(function(d) { return d.id; }).distance(50)) // 斥力 .force("charge", d3.forceManyBody()) // 设置可视化中心。 .force("center", d3.forceCenter(defaultConfig.width / 2, defaultConfig.height / 2)) // 将碰撞力指定为 "collide" 类型的力,使节点保持至少 60 个单位的距离,碰撞的强度为 0.2,使节点之间有一定的距离。 // .iterations(5) 设置迭代次数为 5,提高碰撞的精确度。 .force("collide",d3.forceCollide(61).strength(0.2).iterations(5)) .alphaTarget(1)
-
绘制连接线并添加箭头标记及文本标记:这里对
“好友”
关系作特殊处理(因为data里“好友”
关系占大多数,对关系图的呈现有一定影响,故作特殊处理:连接线为绿色且后续不标注value值关系描述
)。// 创建箭头<marker>元素,之后可以使用该箭头标记在连接线的末端添加箭头效果。 this.vis.append("svg:defs").selectAll("marker") .data(["end"]) // 创建一个 <marker> 元素 .enter().append("svg:marker") .attr("id","arrow") .attr('class','arrow') // 定义了在通过视图呈现时,元素起始点和围绕元素的盒子的属性 .attr("viewBox", "0 -5 10 10") // 在X轴、Y轴方向上的参考点 .attr("refX", 42) .attr("refY", 0) // 宽高 .attr("markerWidth", 7) .attr("markerHeight", 12) .attr("markerUnits","userSpaceOnUse") .attr("orient", "auto") // 添加一个 <path> 元素,用于绘制箭头的路径 .append("svg:path") // 设置路径命令 .attr("d", "M0,-5L10,0L0,5") .attr('fill','#666'); // 绘制连接线(link)并添加箭头标记 this.link = this.vis.selectAll("line.link") .data(defaultConfig.data.links) .enter().append("svg:line") .attr("class", "link") .attr('stroke-width',1.8) // 获取起始点、目标点x,y .attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x; }) .attr("y2", function(d) { return d.target.y; }) .attr("stroke", function(d) { if (d.value === "好友") { return "#007175"; } else { return "gray"; // 其他关系为灰色或默认颜色 } }) .attr("marker-end", "url(#arrow)"); // 为每条连线添加文本元素表示连线的关系描述 this.linetext = this.vis.selectAll('.linetext') .data(defaultConfig.data.links) .enter() .append("text") .attr("class", "linetext") .attr("x", function(d){ return (d.source.x + d.target.x) / 2}) // 文本的水平位置为连线起点和终点x坐标的中间值 .attr("y", function(d){ return (d.source.y + d.target.y) / 2}) // 文本的垂直位置为连线起点和终点y坐标的中间值 .text(function (d) { if (d.value !== "好友") { return d.value; } else { return ""; } // return d.value; }) .call(d3.drag()) // 添加拖拽行为,使文本可以被拖动 .style("font-family", "KaiTi, serif") .style("font-size", "14px")
-
拖拽功能的实现:核心是拖动后引斥力变化后的位置更新。
// 重新计算力导向布局,并刷新可视化效果 this.tick = function() { // 更新连接线的起始点 (x1, y1) 和终点 (x2, y2) 的位置 _this.link.attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x}) .attr("y2", function(d) { return d.target.y;}); _this.linetext.attr("x",function(d){ return (d.source.x + d.target.x) / 2}) .attr("y",function(d){ return (d.source.y + d.target.y) / 2}); // 更新节点的位置 _this.node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); }; _this.force.on("tick", this.tick); // 处理节点的拖拽事件,并创建了一个用于节点拖拽的drag行为 var dragstart = function(d, i) { console.info(d3.event.subject) _this.force.stop(); // 阻止事件传播,以防止与其他事件冲突 d3.event.sourceEvent.stopPropagation(); }; var dragmove = function(d, i) { d.px += d3.event.dx; d.py += d3.event.dy; d.x += d3.event.dx; d.y += d3.event.dy; // 重新计算力导向布局,并刷新可视化效果 _this.tick(); }; var dragend = function(d, i) { d3.event.subject.fx = null; d3.event.subject.fy = null; // 重新启动力导向布局的运行 _this.force.restart(); _this.tick(); }; this.nodeDrag = d3.drag() .on("start", dragstart) .on("drag", dragmove) .on("end", dragend);
-
悬浮框内容设置:这里的悬浮框相当于一个小html,相关样式在css中设置,这里涉及到的鼠标动作目的是确保悬浮框在使用过程中能够正确地显示、隐藏,并且在鼠标交互中具有合适的响应和延迟效果。
// 悬停框显示 this.highlightToolTip = function(obj){ if(obj){ _this.tooltip.html("<div class='title'>" + obj.name + "的详情</div>" + "<table class='detail-info'><tr><td class='td-label'>字号:</td><td>" + obj.zihao + "</td></tr>" + "<tr><td class='td-label'>朝代:</td><td>" + obj.dynasty + "</td></tr>" + "<tr><td class='td-label'>年龄:</td><td>" + obj.age + "</td></tr>" + "<tr><td class='td-label'>称号:</td><td>" + obj.epithet + "</td></tr>" + "<tr><td class='td-label'>事迹:</td><td>" + obj.deeds + "</td></tr>" + "<tr><td class='td-label'>代表作:</td><td>" + obj.famous + "</td></tr>" + "<tr><td class='td-label'>链接:</td><td><a href='" + obj.link + "'>" + obj.name + "的主页</a></td></tr></table>") .style("left",(d3.event.pageX+20)+"px") .style("top",(d3.event.pageY-20)+"px") .style("opacity",1.0); }else{ _this.tooltip.style("opacity",0.0); } }; this.tooltip = d3.select(".network").append("div") .attr("class","tooltip") .attr("opacity",0.0) .on('dblclick',function(){ // 被双击时,阻止提示框冒泡,避免影响其他事件。 d3.event.stopPropagation(); }) .on('mouseover',function(){ // 悬停时,如果之前设置了鼠标移出的定时器,则清除该定时器,以防止工具提示在短时间内被误隐藏 if (_this.node.mouseoutTimeout) { clearTimeout(_this.node.mouseoutTimeout); _this.node.mouseoutTimeout = null; } }) .on('mouseout',function(){ // 当鼠标移出工具提示时,如果之前设置了鼠标移出的定时器,则先清除该定时器。 // 然后,设置一个新的定时器 // 在快速移动鼠标时工具提示不会立即消失,只有在鼠标离开一段时间后才会隐藏。 if (_this.node.mouseoutTimeout) { clearTimeout(_this.node.mouseoutTimeout); _this.node.mouseoutTimeout = null; } _this.node.mouseoutTimeout=setTimeout(function() { _this.highlightToolTip(null); }, 300); });
-
高亮功能实现:核心是找到相关节点,将不相关的节点、连接线、连接文本设置为
inactive
类,搭配css里的透明度设置,实现先关节点的高亮。- 如果传入的对象 obj 存在,该函数将根据该对象相关的节点、连接线和文本进行高亮显示,同时取消其他元素的高亮显示。
- 首先,将传入对象 obj 的索引添加到依赖节点数组 dependsNode 和依赖连接线和文本数组 dependsLinkAndText 中。
- 然后遍历连接线数据,确定与指定对象相关的节点索引,并将这些节点索引添加到依赖节点数组中。
- 最后使用
classed()
方法来根据依赖节点数组和依赖连接线和文本数组的内容,给节点、连接线和文本添加或移除 inactive 类,从而控制它们的显示状态。
- 如果传入的对象 obj 不存在,那么该函数将取消所有元素的高亮显示,即移除所有节点、连接线和文本的 inactive 类。(这里针对搜索框应用(挖个坑,后续补充),如果搜索对象不存在则移除所有
inactive类
,使图表归位)
// 高亮显示与指定对象相关的节点、连接线和文本,并取消其他元素的高亮显示 this.highlightObject = function(obj) { if (obj) { // 获取要高亮显示的对象的索引 var objIndex = obj.index; // 添加到依赖节点数组 dependsNode = dependsNode.concat([objIndex]); // 添加到依赖连接线和文本数组 dependsLinkAndText = dependsLinkAndText.concat([objIndex]); // 遍历连接线数据,确定与指定对象相关的节点索引,并添加到依赖节点数组中 defaultConfig.data.links.forEach(function(lkItem) { if (objIndex == lkItem['source']['index']) { dependsNode = dependsNode.concat([lkItem.target.index]) } else if (objIndex == lkItem['target']['index']) { dependsNode = dependsNode.concat([lkItem.source.index]) } }); _this.node.classed('inactive', function(d) { return dependsNode.indexOf(d.index) == -1; }); _this.link.classed('inactive', function(d) { return dependsLinkAndText.indexOf(d.source.index) == -1 && dependsLinkAndText.indexOf(d.target.index) == -1; }); _this.linetext.classed('inactive', function(d) { return dependsLinkAndText.indexOf(d.source.index) == -1 && dependsLinkAndText.indexOf(d.target.index) == -1; }); } else { _this.node.classed('inactive', false); _this.link.classed('inactive', false); _this.linetext.classed('inactive', false); } };
- 如果传入的对象 obj 存在,该函数将根据该对象相关的节点、连接线和文本进行高亮显示,同时取消其他元素的高亮显示。
-
创建节点元素,绑定数据,绑定鼠标动作对应的
悬浮框效果
和高亮效果
,添加图片图标// 创建节点元素,并绑定相关的数据和事件处理函数 this.node = this.vis.selectAll("g.node") .data(defaultConfig.data.nodes) .enter().append("svg:g") .attr("class", "node") .call(_this.nodeDrag) .on('mouseover', function(d) { // 鼠标悬停在节点上时,悬浮框来 if (_this.node.mouseoutTimeout) { clearTimeout(_this.node.mouseoutTimeout); _this.node.mouseoutTimeout = null; } _this.highlightToolTip(d); }) .on('mouseout', function() { // 鼠标移出节点时,悬浮框走 if (_this.node.mouseoutTimeout) { clearTimeout(_this.node.mouseoutTimeout); _this.node.mouseoutTimeout = null; } _this.node.mouseoutTimeout=setTimeout(function() { _this.highlightToolTip(null); }, 300); }) .on('dblclick',function(d){ // 双击节点时,高亮显示与当前节点相关的节点、连接线和连线上的文本 _this.highlightObject(d); //(阻止事件冒泡) d3.event.stopPropagation(); }); // 为每个节点添加图片元素表示节点图标 this.node.append("svg:image") .data(defaultConfig.data.nodes) .attr("class", "circle") .attr("xlink:href", function(d){ return("./assects/images/" + d.image)}) //以下设置直接绝对图标和连接线箭头的“和平共处”程度 .attr("x", "-25px") .attr("y", "-25px") .attr("width", "50px") .attr("height", "50px");
-
双击空白处复原实现
// 在整个页面上绑定双击事件处理函数 d3.select(".network").on('dblclick',function(){ // 当双击页面其他区域时,取消所有节点、连接线和连线上的文本的高亮显示,并重置依赖节点和连接线数组 dependsNode = dependsLinkAndText = []; _this.highlightObject(null); });