D3.js這個繪圖工具,功能強大不必多說,完全一個Data Driven Document的繪圖工具,用戶可以按照自己的數據以及希望實現的圖形,隨心所欲的繪圖。
圖形繪制,D3默認采用的是異步加載,但是,這里的異步加載,指的是一次性的將圖形展示所需要的數據異步的方式加載到瀏覽器前端顯示。主要有如下這兩種方式:
1 d3.csv(url[[, row], callback]) 2 3 Creates a request for the CSV file at the specified url with the default mime type text/csv. An optional row conversion function may be specified to map and filter row objects to a more-specific representation; see dsv.parse for details. 4 5 The row conversion function can be changed by calling request.row on the returned instance. For example, this: 6 7 d3.csv(url, row, callback); 8 Is equivalent to this: 9 10 d3.csv(url) 11 .row(row) 12 .get(callback);
1 d3.json(url[, callback]) 2 3 Creates a request for the JSON file at the specified url with the default mime type application/json. 4 5 This convenience constructor is approximately equivalent to: 6 7 d3.request(url) 8 .mimeType("application/json") 9 .response(function(xhr) { return JSON.parse(xhr.responseText); }) 10 .get(callback);
上述兩種方式獲取的數據,在很多時候,是比較難滿足實際需求場景的。
比如,我們現在設計的一款微信公眾號的應用中,捕獲關注者轉帖的軌跡,最終以樹狀圖展現給用戶。 若一次性加載所有的數據,會比較影響用戶體驗,因為一次遍歷數據庫所有的跟蹤記錄,無論是遞歸先根遍歷還是非遞歸方式循環查找,最終的體驗都是不令人滿意的。 我們便采取按需的異步加載數據方式,即,當用戶點擊節點時,才從后台取數據。由於D3的優秀數據管理架構,數據一旦加載了,后續便可以不用再從服務器后台取數據。
其實,實現這種on demand方式的異步加載,其實也很簡單。下面就基於官網的一個例子,做點修改,介紹如何實現。
官網原版的例子如下:
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 </style> 26 <body> 27 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> 28 <script> 29 var root = { 30 "name": "flare", 31 "deal": "2", 32 "children": [{ 33 "name": "analytics" , 34 "children": [{ 35 "name": "cluster", 36 "children": [{ 37 "name": "AgglomerativeCluster", 38 "size": 3938 39 }, { 40 "name": "CommunityStructure", 41 "size": 3812 42 }, { 43 "name": "HierarchicalCluster", 44 "size": 6714 45 }, { 46 "name": "MergeEdge", 47 "size": 743 48 }] 49 }] 50 }, { 51 "name": "ISchedulable", 52 "deal": "2", 53 "size": 1041 54 }, { 55 "name": "Parallel", 56 "size": 5176 57 }, { 58 "name": "Pause", 59 "size": 449 60 } 61 ] 62 }; 63 var margin = {top: 20, right: 120, bottom: 20, left: 120}, 64 width = 1024 - margin.right - margin.left, 65 height = 798 - margin.top - margin.bottom; 66 67 var i = 0, 68 duration = 750, 69 root; 70 71 var tree = d3.layout.tree().nodeSize([90, 60]); 72 73 var diagonal = d3.svg.diagonal() 74 .projection(function(d) { return [d.x, d.y]; }); 75 76 /* 77 var svg = d3.select("body").append("svg") 78 .attr("width", width + margin.right + margin.left) 79 .attr("height", height + margin.top + margin.bottom) 80 .append("g") 81 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 82 */ 83 84 //Redraw for zoom 85 function redraw() { 86 //console.log("here", d3.event.translate, d3.event.scale); 87 svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); 88 } 89 90 var svg = d3.select("body").append("svg").attr("width", 1024).attr("height", 798) 91 .call(zm = d3.behavior.zoom().scaleExtent([1,3]).on("zoom", redraw)).append("g") 92 .attr("transform", "translate(" + 512 + "," + 50 + ")"); 93 94 //necessary so that zoom knows where to zoom and unzoom from 95 zm.translate([512, 50]); 96 97 //d3.json("flare.json", function(error, flare) 98 // if (error) throw error; 99 { 100 root.x0 = 0; 101 root.y0 = height / 2; 102 103 function collapse(d) { 104 if (d.children) { 105 d._children = d.children; 106 d._children.forEach(collapse); 107 d.children = null; 108 } 109 } 110 111 root.children.forEach(collapse); 112 update(root); 113 } 114 115 d3.select(self.frameElement).style("height", "800px"); 116 117 function update(source) { 118 119 // Compute the new tree layout. 120 var nodes = tree.nodes(root).reverse(), 121 links = tree.links(nodes); 122 123 debugger; 124 // Normalize for fixed-depth. 125 nodes.forEach(function(d) { d.y = d.depth * 180; }); 126 127 // Update the nodes… 128 var node = svg.selectAll("g.node") 129 .data(nodes, function(d) { return d.id || (d.id = ++i); }); 130 131 // Enter any new nodes at the parent's previous position. 132 var nodeEnter = node.enter().append("g") 133 .attr("class", "node") 134 .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; }) 135 .on("click", click); 136 137 nodeEnter.append("circle") 138 .attr("r", 1e-6) 139 .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); 140 141 nodeEnter.append("text") 142 .attr("x", function(d) { return d.children || d._children ? -10 : 10; }) 143 .attr("dy", ".35em") 144 .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) 145 .text(function(d) { return d.name; }) 146 .style("fill-opacity", 1e-6); 147 148 // Transition nodes to their new position. 149 var nodeUpdate = node.transition() 150 .duration(duration) 151 .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); 152 153 nodeUpdate.select("circle") 154 .attr("r", 20) 155 .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); 156 157 nodeUpdate.select("text") 158 .style("fill-opacity", 1); 159 160 // Transition exiting nodes to the parent's new position. 161 var nodeExit = node.exit().transition() 162 .duration(duration) 163 .attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; }) 164 .remove(); 165 166 nodeExit.select("circle") 167 .attr("r", 1e-6); 168 169 nodeExit.select("text") 170 .style("fill-opacity", 1e-6); 171 172 // Update the links… 173 var link = svg.selectAll("path.link") 174 .data(links, function(d) { return d.target.id; }); 175 176 // Enter any new links at the parent's previous position. 177 link.enter().insert("path", "g") 178 .attr("class", "link") 179 .attr("d", function(d) { 180 var o = {x: source.x0, y: source.y0}; 181 return diagonal({source: o, target: o}); 182 }); 183 184 // Transition links to their new position. 185 link.transition() 186 .duration(duration) 187 .attr("d", diagonal); 188 189 // Transition exiting nodes to the parent's new position. 190 link.exit().transition() 191 .duration(duration) 192 .attr("d", function(d) { 193 var o = {x: source.x, y: source.y}; 194 return diagonal({source: o, target: o}); 195 }) 196 .remove(); 197 198 // Stash the old positions for transition. 199 nodes.forEach(function(d) { 200 d.x0 = d.x; 201 d.y0 = d.y; 202 }); 203 } 204 205 // Toggle children on click. 206 function click(d) { 207 if (d.children) { 208 d._children = d.children; 209 d.children = null; 210 } else { 211 d.children = d._children; 212 d._children = null; 213 } 214 update(d); 215 } 216 217 </script>
下面,再看看,如何實現on demand的異步加載:
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 /* 84 var svg = d3.select("body").append("svg") 85 .attr("width", width + margin.right + margin.left) 86 .attr("height", height + margin.top + margin.bottom) 87 .append("g") 88 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 89 */ 90 91 //Redraw for zoom 92 function redraw() { 93 //console.log("here", d3.event.translate, d3.event.scale); 94 svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); 95 } 96 97 var svg = d3.select("body").append("svg").attr("width", 1024).attr("height", 798) 98 .call(zm = d3.behavior.zoom().scaleExtent([1,3]).on("zoom", redraw)).append("g") 99 .attr("transform", "translate(" + 512 + "," + 50 + ")"); 100 101 //necessary so that zoom knows where to zoom and unzoom from 102 zm.translate([512, 50]); 103 104 //d3.json("flare.json", function(error, flare) 105 // if (error) throw error; 106 107 root.x0 = 0; 108 root.y0 = height / 2; 109 110 function collapse(d) { 111 if (d.children) { 112 d._children = d.children; 113 d._children.forEach(collapse); 114 d.children = null; 115 } 116 } 117 118 root.children.forEach(collapse); 119 update(root); 120 121 122 d3.select(self.frameElement).style("height", "800px"); 123 124 function update(source) { 125 126 // Compute the new tree layout. 127 var nodes = tree.nodes(root).reverse(), 128 links = tree.links(nodes); 129 130 // Normalize for fixed-depth. 131 nodes.forEach(function(d) { d.y = d.depth * 180; }); 132 133 // Update the nodes… 134 var node = svg.selectAll("g.node") 135 .data(nodes, function(d) { return d.id || (d.id = ++i); }); 136 137 // Enter any new nodes at the parent's previous position. 138 var nodeEnter = node.enter().append("g") 139 .attr("class", "node") 140 .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; }) 141 .on("click", click); 142 143 nodeEnter.append("circle") 144 .attr("r", 1e-6) 145 .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); 146 147 nodeEnter.append("text") 148 .attr("cx", function(d) { return d.children || d._children ? -10 : 10; }) 149 .attr("cy", ".35em") 150 .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) 151 .text(function(d) { return d.name; }) 152 .style("fill-opacity", 1e-6); 153 154 // Transition nodes to their new position. 155 var nodeUpdate = node.transition() 156 .duration(duration) 157 .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); 158 159 nodeUpdate.select("circle") 160 .attr("r", 20) 161 .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); 162 163 nodeUpdate.select("text") 164 .style("fill-opacity", 1); 165 166 // Transition exiting nodes to the parent's new position. 167 var nodeExit = node.exit().transition() 168 .duration(duration) 169 .attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; }) 170 .remove(); 171 172 nodeExit.select("circle") 173 .attr("r", 1e-6); 174 175 nodeExit.select("text") 176 .style("fill-opacity", 1e-6); 177 178 // Update the links… 179 var link = svg.selectAll("path.link") 180 .data(links, function(d) { return d.target.id; }); 181 182 // Enter any new links at the parent's previous position. 183 link.enter().insert("path", "g") 184 .attr("class", "link") 185 .attr("d", function(d) { 186 var o = {x: source.x0, y: source.y0}; 187 return diagonal({source: o, target: o}); 188 }); 189 /* 190 console.log(link); 191 192 link.enter().insert("path", "g") 193 .attr("class", function(d){ 194 if(d.source.deal != null && d.source.deal != undefined){ 195 if(d.target.deal != null && d.target.deal != undefined){ 196 return "link2"; 197 } 198 } 199 return "link"; 200 }) 201 .attr("d", function(d) { 202 var o = {x: source.x0, y: source.y0}; 203 return diagonal({source: o, target: o}); 204 }); 205 */ 206 // Transition links to their new position. 207 link.transition() 208 .duration(duration) 209 .attr("d", diagonal); 210 211 // Transition exiting nodes to the parent's new position. 212 link.exit().transition() 213 .duration(duration) 214 .attr("d", function(d) { 215 var o = {x: source.x, y: source.y}; 216 return diagonal({source: o, target: o}); 217 }) 218 .remove(); 219 220 // Stash the old positions for transition. 221 nodes.forEach(function(d) { 222 d.x0 = d.x; 223 d.y0 = d.y; 224 }); 225 } 226 227 function getNode(){ #自定義的一個新的以同步方式從后台取數據的ajax函數 228 var mynodes = null; 229 $.ajax({ 230 url : "./node", 231 async : false, // 注意此處需要同步 232 type : "POST", 233 dataType : "json", 234 success : function(data) { 235 mynodes = data; 236 console.log(mynodes); 237 //nodes = JSON.parse(nodes); 238 } 239 }); 240 return mynodes; 241 } 242 243 // Toggle children on click. 244 function click(d) { #重點關注這個函數的不同之處。尤其是else部分 245 if (d.children) { 246 d._children = d.children; 247 d.children = null; 248 } else if(d._children){ 249 d.children = d._children; 250 d._children = null; 251 }else { 252 var mnodes = getNode(); 253 d.children = mnodes.children; 254 } 255 update(d); 256 } 257 258 </script>
配合這個ajax的函數,java后台的代碼,其實非常的簡單,為了測試,構建D3樹狀圖所需的數據結構。主要都是Array (含有孩子節點)
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 "d3demo"; 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 }
有一定的參考價值,需要的請轉載,轉載指明出處!