D3作为前端图形显示的利器,功能之强,对底层技术细节要求相对比较多。
有一点,就是要理解其基本的数据和节点的匹配规则架构,即enter,update和exit原理,我前面的D3基础篇中有介绍过,不明白的可以再去研究下。
本篇博文,同样是在这个框架下,完成修改树状图中某两个节点之间的边用红色线条连接,实现表达特殊含义的目的。
背景故事: 微信朋友圈之间产品帖子相互转发,有些帖子转发后会有成交,只要有成交,则这个促成成交的节点及其之上的父节点都相应是有功劳的,这个轨迹需要用高亮的颜色表示(比如本例中,用红色表示)。
其实也比较简单,直接看代码, 前端部分:
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <style> 4 5 .node { 6 cursor: pointer; 7 } 8 9 .node circle { 10 fill: #fff; 11 stroke: steelblue; 12 stroke-width: 1.5px; 13 } 14 15 .node text { 16 font: 10px sans-serif; 17 } 18 19 .link { 20 fill: none; 21 stroke: #ccc; 22 stroke-width: 1.5px; 23 } 24 25 .link2 { 26 fill: none; 27 stroke: #f00; 28 stroke-width: 1.5px; 29 } 30 31 </style> 32 <body> 33 <script src="js/jquery-2.1.1.min.js" charset="utf-8"></script> 34 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> 35 <script> 36 var root = { 37 "name": "flare", 38 "deal": "2", 39 "children": [{ 40 "name": "analytics" , 41 "children": [{ 42 "name": "cluster", 43 "children": [{ 44 "name": "AgglomerativeCluster", 45 "size": 3938 46 }, { 47 "name": "CommunityStructure", 48 "size": 3812 49 }, { 50 "name": "HierarchicalCluster", 51 "size": 6714 52 }, { 53 "name": "MergeEdge", 54 "size": 743 55 }] 56 }] 57 }, { 58 "name": "ISchedulable", 59 "deal": "2", 60 "size": 1041 61 }, { 62 "name": "Parallel", 63 "size": 5176 64 }, { 65 "name": "Pause", 66 "size": 449 67 } 68 ] 69 }; 70 var margin = {top: 20, right: 120, bottom: 20, left: 120}, 71 width = 1024 - margin.right - margin.left, 72 height = 798 - margin.top - margin.bottom; 73 74 var i = 0, 75 duration = 750, 76 root; 77 78 var tree = d3.layout.tree().nodeSize([90, 60]); 79 80 var diagonal = d3.svg.diagonal() 81 .projection(function(d) { return [d.x, d.y]; }); 82 83 //Redraw for zoom 84 function redraw() { 85 //console.log("here", d3.event.translate, d3.event.scale); 86 svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); 87 } 88 89 var svg = d3.select("body").append("svg").attr("width", 1024).attr("height", 798) 90 .call(zm = d3.behavior.zoom().scaleExtent([1,3]).on("zoom", redraw)).append("g") 91 .attr("transform", "translate(" + 512 + "," + 50 + ")"); 92 93 //necessary so that zoom knows where to zoom and unzoom from 94 zm.translate([512, 50]); 95 96 //d3.json("flare.json", function(error, flare) 97 // if (error) throw error; 98 99 root.x0 = 0; 100 root.y0 = height / 2; 101 102 function collapse(d) { 103 if (d.children) { 104 d._children = d.children; 105 d._children.forEach(collapse); 106 d.children = null; 107 } 108 } 109 110 root.children.forEach(collapse); 111 update(root); 112 113 114 d3.select(self.frameElement).style("height", "800px"); 115 116 function update(source) { 117 118 // Compute the new tree layout. 119 var nodes = tree.nodes(root).reverse(), 120 links = tree.links(nodes); 121 122 // Normalize for fixed-depth. 123 nodes.forEach(function(d) { d.y = d.depth * 180; }); 124 125 // Update the nodes… 126 var node = svg.selectAll("g.node") 127 .data(nodes, function(d) { return d.id || (d.id = ++i); }); 128 129 // Enter any new nodes at the parent's previous position. 130 var nodeEnter = node.enter().append("g") 131 .attr("class", "node") 132 .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; }) 133 .on("click", click); 134 135 nodeEnter.append("circle") 136 .attr("r", 1e-6) 137 .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); 138 139 nodeEnter.append("text") 140 .attr("cx", function(d) { return d.children || d._children ? -10 : 10; }) 141 .attr("cy", ".35em") 142 .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) 143 .text(function(d) { return d.name; }) 144 .style("fill-opacity", 1e-6); 145 146 // Transition nodes to their new position. 147 var nodeUpdate = node.transition() 148 .duration(duration) 149 .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); 150 151 nodeUpdate.select("circle") 152 .attr("r", 20) 153 .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); 154 155 nodeUpdate.select("text") 156 .style("fill-opacity", 1); 157 158 // Transition exiting nodes to the parent's new position. 159 var nodeExit = node.exit().transition() 160 .duration(duration) 161 .attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; }) 162 .remove(); 163 164 nodeExit.select("circle") 165 .attr("r", 1e-6); 166 167 nodeExit.select("text") 168 .style("fill-opacity", 1e-6); 169 170 // Update the links… 171 var link = svg.selectAll("path.link") 172 .data(links, function(d) { return d.target.id; }); 173 174 // Enter any new links at the parent's previous position. 175 link.enter().insert("path", "g") 176 .attr("class", "link") 177 .attr("d", function(d) { 178 var o = {x: source.x0, y: source.y0}; 179 return diagonal({source: o, target: o}); 180 }); 181 182 // Transition links to their new position. 183 link.transition() 184 .duration(duration) 185 .attr("d", diagonal) 186 .attr("class", function(d){ 187 if(d.source.deal != null && d.source.deal != undefined){ 188 if(d.target.deal != null && d.target.deal != undefined){ 189 return "link link2"; 190 } 191 } 192 return "link"; 193 }); 194 195 // Transition exiting nodes to the parent's new position. 196 link.exit().transition() 197 .duration(duration) 198 .attr("d", function(d) { 199 var o = {x: source.x, y: source.y}; 200 return diagonal({source: o, target: o}); 201 }) 202 .remove(); 203 204 // Stash the old positions for transition. 205 nodes.forEach(function(d) { 206 d.x0 = d.x; 207 d.y0 = d.y; 208 }); 209 } 210 211 212 function getNode(){ 213 var mynodes = null; 214 $.ajax({ 215 url : "./node", 216 async : false, // 注意此处需要同步,因为返回完数据后,下面才能让结果的第一条selected 217 type : "POST", 218 dataType : "json", 219 success : function(data) { 220 mynodes = data; 221 console.log(mynodes); 222 //nodes = JSON.parse(nodes); 223 } 224 }); 225 return mynodes; 226 } 227 228 // Toggle children on click. 229 function click(d) { 230 if (d.children) { 231 d._children = d.children; 232 d.children = null; 233 } else if(d._children){ 234 d.children = d._children; 235 d._children = null; 236 }else { 237 var mnodes = getNode(); 238 d.children = mnodes.children; 239 } 240 update(d); 241 } 242 243 </script>
整个前端的代码,重点看其中红色标识的部分,这些部分是和这个博文的内容直接相关的。 涉及到连接的红色标识。 数据中定义了deal字段,这个字段就是标识某个节点具有这个特性,只有这个特性的节点之间的边用红色标识。 另外,点击按钮,异步加载后端服务器的代码部分,也有部分数据是含有这个deal特性的,同样适用于本故事的要求。
在代码的186行中,link的transition(即变化,变换)过程中,去渲染节点之间的边的样式。其实,还可以在其他地方做这个样式的加载,比如在link的enter部分实现,只是这个过程,有点违背D3架构设计之enter,update和exit的大前提,不建议在enter里面实现这个功能。
后端的代码,只是一个示例代码,和前面D3博文的基本相同:
1 /** 2 * @author "shihuc" 3 * @date 2016年11月14日 4 */ 5 package com.tk.es.search.controller; 6 7 import java.util.ArrayList; 8 import java.util.HashMap; 9 10 import javax.servlet.http.HttpServletRequest; 11 12 import org.springframework.stereotype.Controller; 13 import org.springframework.web.bind.annotation.RequestMapping; 14 import org.springframework.web.bind.annotation.ResponseBody; 15 16 import com.google.gson.Gson; 17 18 /** 19 * @author chengsh05 20 * 21 */ 22 @Controller 23 public class D3Controller { 24 25 @RequestMapping(value = "/d3") 26 public String d3Page(HttpServletRequest req){ 27 return "d3demo2"; 28 } 29 30 @RequestMapping(value = "/node") 31 @ResponseBody 32 public String asyncGet(HttpServletRequest req){ 33 HashMap<String, Object> data = new HashMap<String, Object>(); 34 ArrayList<Object>elem1 = new ArrayList<Object>(); 35 HashMap<String, String> elem1e = new HashMap<String, String>(); 36 elem1e.put("name", "one"); 37 elem1e.put("deal", "2"); 38 HashMap<String, String> elem2e = new HashMap<String, String>(); 39 elem2e.put("name", "two"); 40 HashMap<String, String> elem3e = new HashMap<String, String>(); 41 elem3e.put("name", "three"); 42 elem1.add(elem1e); 43 elem1.add(elem2e); 44 elem1.add(elem3e); 45 46 data.put("name", "Pause"); 47 data.put("children", elem1); 48 49 Gson gson = new Gson(); 50 return gson.toJson(data); 51 } 52 }
上述代码,表明只有节点名为one的节点,给其配置deal属性值,也就是说在最终D3绘制的树状图上,名为one的节点间会出现红色link。
最终效果图如下,首先看默认显示的情况
点击显示one节点之间的状态