http://www.demodashi.com/demo/13181.html
概述
詳細
人物關系知識圖譜
一、背景
將結構化數據通過關系預處理程序處理為圖數據庫可以查詢的數據,示例是將其中一部分(人物關系數據)可視化表示。
二、用到的技術
技術點:圖數據庫Neo4j,d3.js,java,css,spring boot
開發工具:IDEA專業版(可找學生賬號注冊免費使用一年,社區版不支持WEB開發)
三、項目結構以及代碼實現過程
實現思路這樣:
1,先定義基礎的展示頁面index.html
2、完成畫圖js(graph.js)
3,提供一個基礎的拿數據接口加載測試繪圖數據和繪圖需要的數據(例如節點選中之后的小圖標加載)
4、頁面從數據接口請求數據之后,調用繪圖JS在頁面完成畫圖操作(請求數據的接口可以很方便的改為從圖數據庫拿取數據進行展示)
主要文件目錄說明:
1、data目錄
bg.jpg可視化背景圖片數據
CircularPartition.json節點圓形分區圖工具欄需要加載的數據
test.json可視化需要展示的數據格式2、images
此目錄存儲節點屬性圖片數據3、js
d3.js version-3.2.84、src
JS以及其它HTML等源碼5、index.html
知識圖譜可視化入口文件6、拿數據接口
通過數據Type id加載圓形分區圖數據和測試知識圖譜構圖數據(type等於1加載圓形分區數據,type是等於2加載測試知識圖譜展示數據)
GET:http://localhost:7476/knowledge-graph/hello/dataSource/type/{id}
做圖過程(graph.js):
// 定義畫布 (radius是鼠標點擊生成圓形分區圖的半徑)var width = 1345, height = 750, color = d3.scale.category20();var svg = d3.select("body").append("svg").attr("id", "svgGraph").attr("width", width).attr("height", height).append("g").attr("id", "svgOne").call(d3.behavior.zoom() // 自動創建事件偵聽器.scaleExtent([0.1, 10]) // 縮放允許的級數.on("zoom", zoom)).on("dblclick.zoom", null); // remove雙擊縮放
// 實時獲取SVG畫布坐標function printPosition() {var position = d3.mouse(svg.node());return position;}
// 縮放函數function zoom() {// translate變換矢量(使用二元組標識)scale當前尺度的數字svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); // 畫布縮放與移動// svg.attr("transform", "scale(" + d3.event.scale + ")"); // 畫布縮放}
// 設置連線箭頭屬性function setMarkers() {svg.append("g").attr("id", "lineAndText").selectAll("marker").data(edges).enter().append("marker").attr("id", function (d) {return d.id;}).attr("viewBox", "0 -5 10 10") // 坐標系的區域.attr("class", "arrow").attr("refX", 27) // refX,refY在viewBox內的基准點,繪制時此點在直線端點上(要注意大小寫).attr("refY", 0).attr("markerWidth", 10) // 標識的大小.attr("markerHeight", 18) // 標識的大小.attr("markerUnits", "userSpaceOnUse") // 標識大小的基准,有兩個值:strokeWidth(線的寬度)和userSpaceOnUse(圖形最前端的大小).attr("orient", "auto") // 繪制方向,可設定為:auto(自動確認方向)和 角度值.append("path").attr("d", "M0,-5L10,0L0,5").attr("fill", "#ccc");}
// 添加連線function add_edges() {setMarkers(); // 設置連線箭頭屬性var svg_edges = svg.select("#lineAndText").selectAll("line").data(edges).enter().append("line").attr("id", function (d) {return d.id;}).style("stroke", "#ccc").style("stroke_width", 1).attr("marker-end", function (d) {return "url(#" + d.id + ")";}).attr("stroke", "#999").on("mouseover", function (d) { // 鼠標選中時觸發mouseSelectLine(d);addToolTip(d); //添加提示框的div}).on("mouseout", function () {d3.select("#relation").remove();d3.select("#tooltip").remove();});return svg_edges;}
// 求直線與圓的交點// 函數參數說明:cx:圓X軸坐標 cy:圓y軸坐標 r:圓半徑 stx:起點直線的X軸坐標 sty:起點直線的軸坐標 edx:終點直線的X軸坐標 edy:終點直線的Y軸坐標// 返回值:交點坐標(x,y)function getPoint(cx, cy, r, stx, sty, edx, edy) {// 求直線var k = (edy - sty) / (edx - stx);var b = edy - k * edx;//列方程var x1, y1, x2, y2;var c = cx * cx + (b - cy) * (b - cy) - r * r;var a = (1 + k * k);var b1 = (2 * cx - 2 * k * (b - cy));var tmp = Math.sqrt(b1 * b1 - 4 * a * c);x1 = (b1 + tmp) / (2 * a);y1 = k * x1 + b;x2 = (b1 - tmp) / (2 * a);y2 = k * x2 + b;// 過濾距離最近的坐標var p = {};function lineIf(lx, ly, lxx, lyy) {var d = Math.sqrt((lx - lxx) * (lx - lxx) + (ly - lyy) * (ly - lyy));return d;}if (cx != stx) { // stx, styvar d1 = lineIf(x1, y1, stx, sty);var d2 = lineIf(x2, y2, stx, sty);if (d1 < d2) {p.x = x1;p.y = y1;} else {p.x = x2;p.y = y2;}} else { // edx, edyvar d1 = lineIf(x1, y1, edx, edy);var d2 = lineIf(x2, y2, edx, edy);if (d1 < d2) {p.x = x1;p.y = y1;} else {p.x = x2;p.y = y2;}}return p;}
// 鼠標選中關系添加顯示效果function mouseSelectLine(d) {var p1 = getPoint(d.source.x, d.source.y, 20, d.source.x, d.source.y, d.target.x, d.target.y);var p2 = getPoint(d.target.x, d.target.y, 20, d.source.x, d.source.y, d.target.x, d.target.y);var json = [p1, p2];//構造默認線性生成器var line = d3.svg.line().x(function (d) { //指定x存取器為:取每個數據元素的x屬性的值return d.x;}).y(function (d) { //指定y存取器為:取每個數據元素的y屬性的值return d.y;});svg.append('path').attr({"d": function () { //生成路徑數據return line(json);},"id": "relation"}).style({"stroke": "#87CEFA", //path顏色"stroke-width": 6 //path粗細});}
// 添加節點function add_nodes() {var svg_nodes = svg.append("g").attr("id", "circleAndText").selectAll("circle").data(nodes).enter().append("g").call(force.drag().on("dragstart", function (d) {d3.select("#eee").remove(); // 刪除節點扇形d3.select("#sel").remove(); // 刪除節點選中d3.event.sourceEvent.stopPropagation(); // 畫布拖動與節點拖動分離d3.select(this).attr("r", 20 * 2);}).on("dragend", function (d) {d3.select("#eee").remove(); // 刪除節點扇形d3.select("#sel").remove(); // 刪除節點選中d.fixed = true; // 拖動結束后節點固定d3.select(this).attr("r", 20);})).on("click", function (d) { // 鼠標點擊時觸發// 在當前節點處畫三頁扇形d3.select("#eee").remove();drawCirclePartition(d);}).on("mouseover", function (d) { // 光標放在某元素上smouseSelect(d); // 鼠標選中效果addToolTip(d); //添加提示框的div}).on("mouseout", function (d) {d3.select("#sel").remove(); // 刪除節點選中d3.select("#tooltip").remove();d3.select("#tooltipCir").remove();});svg_nodes.append("circle").attr("id", function (d) {return d.index;}).attr("r", 20).attr("fill", function (d, i) {return color(i);});svg_nodes.append("image").attr("class", "circle").attr("xlink:href", function (d) {var img = d.image;if (img != undefined) {return "http://222.216.195.154:7476/knowledge-graph/path/images/" + d.image} else {return null;}}).attr("x", "-20px").attr("y", "-20px").attr("width", "40px").attr("height", "40px");svg_nodes.append("svg:text").style("fill", "#ccc").attr("dx", 20).attr("dy", 8).text(function (d) {return d.name});return svg_nodes;}
//添加提示框的divfunction addToolTip(d) {var htmlStr;if (d.source && d.target && d.type) {htmlStr = "name:" + d.type + "<br/>";} else {htmlStr = "id:" + d.id + "<br/>" + "name:" + d.name + "<br/>";}var position = printPosition(d);var tooltip = d3.select("body").append("div").attr("class", "tooltip") //用於css設置類樣式.attr("opacity", 0.0).attr("id", "tooltip");htmlStr = htmlStr + "locx:" + position[0] + "<br/>" + "locy:" + position[1] + "<br/>";if (d.image != undefined) {htmlStr = htmlStr + "<img src=\"http://222.216.195.154:7476/knowledge-graph/path/images/" + d.image + "\" height=\"100\" width=\"100\" />";}tooltip.html(htmlStr).style("left", (d3.event.pageX) + "px").style("top", (d3.event.pageY + 20) + "px").style("opacity", 0.75);}function addToolTipCir(d) {var htmlStr;if (d.name == "☿") {htmlStr = "notes:解鎖當前節點<br/>";}if (d.name == "✂") {htmlStr = "notes:裁剪當前節點與關系<br/>";}if (d.name == "✠") {htmlStr = "notes:拓展當前節點與關系<br/>";}if (d.name == "◎") {htmlStr = "notes:釋放所有鎖定的節點<br/>";}if (d.name == "오") {htmlStr = "notes:鎖定所有節點<br/>";}var tooltip = d3.select("body").append("div").attr("class", "tooltip") //用於css設置類樣式.attr("opacity", 0.0).attr("id", "tooltipCir");tooltip.html(htmlStr).style("left", (d3.event.pageX) + "px").style("top", (d3.event.pageY + 20) + "px").style("opacity", 0.75);}
// 生成圓弧需要的角度數據var arcDataTemp = [{startAngle: 0, endAngle: 2 * Math.PI}];var arc_temp = d3.svg.arc().outerRadius(26).innerRadius(20);
// 鼠標選中節點添加顯示效果var svg_selectNode;function mouseSelect(d) {svg_selectNode = svg.append("g").attr("id", "sel").attr("transform", "translate(" + d.x + "," + d.y + ")").selectAll("path.arc").data(arcDataTemp).enter().append("path").attr("fill", "#87CEFA").attr("d", function (d, i) {return arc_temp(d, i);});}
// 全局停止力作用之間的影響function stopForce() {for (var i = 0; i < nodes.length; i++) {var obj = nodes[i];obj.fixed = true;}}
// 全局開始力作用之間的影響function startForce() {for (var i = 0; i < nodes.length; i++) {var obj = nodes[i];obj.fixed = false;}force.resume();}
var nodesMark = [], edgesMark = [], indexNodeMark = []; // 緩存中所有已加載的數據標記// 節點添加圓形分區(添加三頁扇形)function drawCirclePartition(d) {// 圓形分區布局(數據轉換)var radius = 40;var partition = d3.layout.partition().sort(null).size([2 * Math.PI, radius * radius]) // 第一個值域時2 PI,第二個值時圓半徑的平方.value(function (d) {return 1;});// 繪制圓形分區圖// 如果以圓形的形式來轉換數據那么d.x和d.y分別代表圓弧的繞圓心// 方向的起始位置和由圓心向外的起始位置d.dx和d.dy分別代表各自的寬度var arc = d3.svg.arc().startAngle(function (d) {return d.x;}).endAngle(function (d) {return d.x + d.dx;}).innerRadius(function (d) {return 26;}).outerRadius(function (d) {return 80;});var circlePart = partition.nodes(dataCirclePartition);// "☿" 釋放固定的節點function releaseNode() {d.fixed = false;// force.start(); // 開啟或恢復結點間的位置影響force.resume();}// "✂" 刪除當前節點以及當前節點到其它節點之間的關系function removeNode() {var newNodes = [];for (var i = 0; i < nodes.length; i++) {var obj = nodes[i];if (obj.id != d.id) {newNodes.push(obj);}}var newedges = [];for (var i = 0; i < edges.length; i++) {var obj = edges[i];if ((d.index != obj.source.index) && (d.index != obj.target.index)) {newedges.push(obj);}}nodes = newNodes;edges = newedges;var nIndex = function (d) {return d.index;};var lIndex = function (d) {return d.id;};// 通過添加'g'元素分組刪除svg.select("#circleAndText").selectAll("circle").data(nodes, nIndex).exit().remove();svg.select("#circleAndText").selectAll("image").data(nodes, nIndex).exit().remove();svg.select("#circleAndText").selectAll("text").data(nodes, nIndex).exit().remove();svg.select("#lineAndText").selectAll("line").data(edges, lIndex).exit().remove();svg.select("#lineAndText").selectAll("text").data(edges, lIndex).exit().remove();}
// 擴展當前節點,距離為1// 1.從rawData(rawNodes/rawEdges)中找出當前節點需要擴展的節點與關系數據// 2.拿出需要擴展的數據到node/edges中去除已經綁定圖形元素的數據// 3.將過濾出的未綁定圖形元素需要擴展的數據重新調用構圖方法進行構圖// 添加從服務器實時加載數據的功能:基本思想與1~3類似function extendNode() {var index = d.index;var arrEdges = [], arrIndex = [], arrNodes = [];for (var i = 0; i < rawEdges.length; i++) {if ((index == rawEdges[i].source.index) || (index == rawEdges[i].target.index)) {arrEdges.push(rawEdges[i]);if (index != rawEdges[i].source.index) {arrIndex.push(rawEdges[i].source.index);} else if (index != rawEdges[i].target.index) {arrIndex.push(rawEdges[i].target.index);}}edgesMark.push(rawEdges[i].id);}for (var i = 0; i < rawNodes.length; i++) {for (var j = 0; j < arrIndex.length; j++) {var obj = arrIndex[j];if (rawNodes[i].index == obj) {arrNodes.push(rawNodes[i]);}}nodesMark.push(rawNodes[i].id);indexNodeMark.push(rawNodes[i].index);}// nodes.push(arrNodes);// edges.push(arrEdges);var nodesRemoveIndex = [];for (var i = 0; i < arrNodes.length; i++) {var obj = arrNodes[i];for (var j = 0; j < nodes.length; j++) {var obj2 = nodes[j];if (obj.index == obj2.index) {nodesRemoveIndex.push(i);}}}var edgesRemoveIndex = [];for (var i = 0; i < arrEdges.length; i++) {var obj = arrEdges[i];for (var j = 0; j < edges.length; j++) {var obj2 = edges[j];if (obj.id == obj2.id) {edgesRemoveIndex.push(obj.id);}}}var coverNodes = [];for (var i = 0; i < arrNodes.length; i++) {var obj = arrNodes[i];if (!isInArray(nodesRemoveIndex, i)) {nodes.push(obj);coverNodes.push(obj);}}var coverEdges = [];for (var i = 0; i < arrEdges.length; i++) {var obj = arrEdges[i];if (!isInArray(edgesRemoveIndex, obj.id)) {edges.push(obj);coverEdges.push(obj);}}// console.log("找出需要擴展的數據");// console.log(arrEdges);// console.log(arrNodes);// console.log("添加到原始需要綁定圖形元素的數據集集合/與rawNodes,rawEdges服務器加載的原始數據保持區分");// console.log(nodes);// console.log(edges);// 添加從服務器請求擴展數據// var url = "http://222.216.195.154:7476/knowledge-graph/hello/dataSource/node/extend/" + d.id + "";// d3.json(url, function (error, json) { // 服務器加載知識圖譜數據// if (error) {// return console.warn(error);// }// console.log("從服務器請求的擴展數據:");// var serverNodes = json.nodes;// var serverEdges = json.links;// console.log(serverNodes);// console.log(serverEdges);// console.log(nodesMark);// console.log(edgesMark);// // 重新設置INDEX// var maxIndex = Math.max.apply(null, indexNodeMark);// console.log("MAX:" + maxIndex);//// for (var i = 0; i < serverNodes.length; i++) {// if (!isInArray(nodesMark, serverNodes[i].id)) {// serverNodes[i].index = maxIndex + 1// maxIndex = maxIndex + 1;// nodes.concat(serverNodes[i]);// console.log(serverNodes[i]);// }// }// for (var i = 0; i < serverEdges.length; i++) {// if (!isInArray(edgesMark, serverEdges[i].id)) {// edges.concat(serverEdges);// console.log(serverEdges[i]);// }// }// console.log("服務器加載並且合並之后的數據:");// console.log(nodes);// console.log(edges);// d3.select("#svgGraph").select("#svgOne").selectAll("*").remove(); // 清空SVG中的內容// buildGraph();// });d3.select("#svgGraph").select("#svgOne").selectAll("*").remove(); // 清空SVG中的內容buildGraph();}var arcs = svg.append("g").attr("id", "eee").attr("transform", "translate(" + d.x + "," + d.y + ")").selectAll("g").data(circlePart).enter().append("g").on("click", function (d) { // 圓形分區綁定Click事件if (d.name == "☿") {releaseNode();}if (d.name == "✂") {removeNode();}if (d.name == "✠") {extendNode();}if (d.name == "◎") {startForce();}if (d.name == "오") {stopForce();}d3.select("#eee").remove();d3.select("#tooltipCir").remove();});arcs.append("path").attr("display", function (d) {return d.depth ? null : "none"; // hide inner ring}).attr("d", arc).style("stroke", "#fff").style("fill", "#A9A9A9").on("mouseover", function (d) {d3.select(this).style("fill", "#747680");addToolTipCir(d); //添加提示框的div}).on("mouseout", function () {d3.select("#tooltipCir").remove();d3.select(this).transition().duration(200).style("fill", "#ccc")var array = printPosition();var distance = Math.sqrt(Math.pow((d.x - array[0]), 2) + Math.pow((d.y - array[1]), 2));if (distance > 80) {d3.select("#eee").remove(); // 刪除節點扇形}});arcs.append("text").style("font-size", "16px").style("font-family", "simsun").style("fill", "white").attr("text-anchor", "middle").attr("transform", function (d, i) {// 平移和旋轉var r = 0;if ((d.x + d.dx / 2) / Math.PI * 180 < 180) // 0-180度以內的r = 180 * ((d.x + d.dx / 2 - Math.PI / 2) / Math.PI);else // 180-360度r = 180 * ((d.x + d.dx / 2 + Math.PI / 2) / Math.PI);return "translate(" + arc.centroid(d) + ")" + "rotate(" + r + ")";}).text(function (d) {return d.name;});return arcs;}
// 添加描述關系文字function add_text_edges() {var svg_text_edges = svg.select("#lineAndText").selectAll("line.text").data(edges).enter().append("text").attr("id", function (d) {return d.id;}).style("fill", "#ccc").attr("x", function (d) {return (d.source.x + d.target.x) / 2}).attr("y", function (d) {return (d.source.y + d.target.y) / 2}).text(function (d) {return d.type;}).on("mouseover", function (d) { // 鼠標選中時觸發mouseSelectLine(d);addToolTip(d); //添加提示框的div}).on("mouseout", function () {d3.select("#relation").remove();d3.select("#tooltip").remove();}).on("click", function () {});return svg_text_edges;}
// 對於每一個時間間隔進行更新function refresh() {force.on("tick", function () { // 對於每一個時間間隔// 更新連線坐標·svg_edges.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;});// 更新節點以及文字坐標svg_nodes.attr("transform", function (d) {return "translate(" + d.x + "," + d.y + ")";});// 更新關系文字坐標svg_text_edges.attr("x", function (d) {return (d.source.x + d.target.x) / 2}).attr("y", function (d) {return (d.source.y + d.target.y) / 2});});}
var force, nodes = [], edges = [], rawNodes, rawEdges, mapNodes = new Map(); // 構建知識圖譜需要操作的數據 (rawNodes, rawEdges將加載的原始構圖數據緩存一份)// 知識圖譜可視化構建function graph(data) {// 先清空布局中的圖形元素// d3.select("#svgGraph").select("#svgOne").selectAll("*").remove();// var serverD = data.nodes;// var serverE = data.links;// 去除NODES中重復的節點,如果有節點重復即將EDGES中的數據重新設置source值和target值// serverD,serverE,nodes,edges// var filterServerD = [];// for (var i = 0; i < serverD.length; i++) {// if (!isInArray(nodesIndexId, serverD[i].id)) {// filterServerD.push(serverD[i]);// }// }// 去重之后重新調整filterServerD的NODE index值// mapNodes.forEach(function (value, key) {// console.log(value);// console.log(key);// if (isInArray(nodesIndexId,key)){//// }// });// recordNodesIndex(serverD);// console.log(nodesIndexValue);// 多數組連接// nodes = nodes.concat(data.nodes);// edges = edges.concat(data.links);// console.log(nodes);// console.log(edges);// rawNodes = nodes;// rawEdges = edges;// // 定義力布局(數據轉換)// force = d3.layout.force()// .nodes(nodes) // 指定節點數組// .links(edges) // 指定連線數組// .size([width, height]) // 指定范圍// .linkDistance(150) // 指定連線長度// // .gravity(0.02) // 設置引力避免躍出布局// .friction(0.9) // 設置摩擦力速度衰減// .charge(-400); // 相互之間的作用力// force.start(); // 開始作用// buildGraph();}
var svg_edges, svg_nodes, svg_text_edges; // 需要動態更新的函數(dynamic update function)// Strat build Knowledge Graph/Vaultfunction buildGraph() {console.log("開始構建可視化知識圖譜:");console.log(nodes);console.log(edges);svg_edges = add_edges(); // 添加連線與箭頭svg_nodes = add_nodes(); // 添加節點與文字svg_text_edges = add_text_edges(); // 添加描述關系的文字refresh(); // 對於每一個時間間隔進行更新force.resume(); // 必須添加否則圖形元素更新不及時}
// 服務器加載數據var dataCirclePartition;function load() {d3.json("http://222.216.195.154:7476/knowledge-graph/hello/dataSource/type/1", function (error, root) { // 服務器加載節點圓形分區數據if (error) {return console.warn(error);}dataCirclePartition = root;});d3.json("http://222.216.195.154:7476/knowledge-graph/hello/dataSource/type/2", function (error, json) { // 服務器加載知識圖譜數據if (error) {return console.warn(error);}console.log("初始加載:");console.log(json.nodes);console.log(json.links);graph(json);});// d3.json("http://222.216.195.154:7476/knowledge-graph/hello/dataSource/node/extend/99817", function (error, json) { // 服務器加載知識圖譜數據// if (error) {// return console.warn(error);// }// console.log("初始加載:");// console.log(json);// graph(json);// });}// 初始化圖數據庫配置信息startNeo4j();// 執行知識圖譜數據可視化load();// 傳入NODE ID與NODE INDEX,節點的INDEX與構圖時數據加載的順序密切相關function loadById(id, maxNodeIndex, nodesIdList) {// var para = ["id:" + id, "maxNodeIndex:" + maxNodeIndex, "nodesIdList:" + nodesIdList];var para = {"id": id, "maxNodeIndex": maxNodeIndex, "nodesIdList": nodesIdList};console.log(para);d3.json("http://222.216.195.154:7476/knowledge-graph/hello/dataSource/node/idIndex/" + para, function (error, data) { // 服務器加載知識圖譜數據if (error) {return console.warn(error);}console.log("動態ID加載的數據:");console.log(nodesMark);console.log(edgesMark);console.log(nodes);console.log(edges);console.log(data);graph(data);});}function loadZdrSearch(json) {d3.json("http://222.216.195.154:7476/knowledge-graph/hello/dataSource/type/1", function (error, root) { // 服務器加載節點圓形分區數據if (error) {return console.warn(error);}dataCirclePartition = root;});graph(json);}// 執行知識圖譜數據可視化// loadById(id);
啟動入口類KnowledgeGraphApplication之后,
調用接口:http://localhost:7476/knowledge-graph/hello/index
此接口調用控制類加載index.html,HTML中調用了js文件加載展示數據,詳細的實現過程請看完整的代碼注釋。
代碼目錄結構說明一:
代碼目錄結構說明二:
四、可視化效果(所有可視化效果均帶有力布局效果)
1.節點與關系均帶有選中效果,節點關系裁剪與擴展
☿ 解鎖當前節點
✂ 剪切當前節點於關系
✠ 擴展當前節點與關系
오 固定所有節點
◎ 解鎖所有節點
實體擴展功能:
節點效果:
2.完整示例
完整效果顯示示例一:
完整效果顯示示例二:






