Created
August 21, 2015 13:53
-
-
Save Rayraegah/73a55147d6962f4636f6 to your computer and use it in GitHub Desktop.
hierarchical-classification
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function graph(d3) { | |
//step 0, new graph() ,import "http://d3js.org/d3.v3.min.js" to get d3 | |
//step 1, custom the config | |
this.config = { | |
bg_size: { | |
width: 800, | |
height: 600 | |
}, | |
edge_def_width: 5, | |
edge_show_arrow: true, | |
node_draggable: true, | |
show_performance_bar: false, | |
} | |
var self = this; | |
var cluster = d3.layout.cluster().size([self.config.bg_size.height, self.config.bg_size.width - 160]); | |
/// step 2, custom the actions | |
var showTitleAction; | |
var showSubheadAction; | |
var showPathDesc; | |
this.showTitle = function (f) { | |
showTitleAction = f; | |
} | |
this.showSubhead = function (f) { | |
showSubheadAction = f; | |
} | |
this.showPathDesc = function (f) { | |
showPathDesc = f; | |
} | |
/// final step , bind some data | |
this.bind = function (data) { | |
/** | |
忽略连通图中的回路,产生一棵树。 | |
这棵树符合cluster.nodes(tree)的调用要求(参见:https://github.com/mbostock/d3/wiki/Cluster-Layout) | |
*/ | |
var conv2tree = function (data) { | |
var root = self.getRoot(data); | |
var hasParentFlag = {}; //保证每个节点只有一个父节点,以便形成树状结构 | |
hasParentFlag[root.id] = true; //根节点不允许作为子节点 | |
self.traverseEdge(data, function (source, target) { //遍历每条边,即所有节点间关系 | |
if (!hasParentFlag[target.id] && source.id != target.id) { //首次被遍历到的target,作为source的子节点,后续将不被其它节点作为子节点 | |
if (!source.children) { | |
source.children = []; | |
} | |
source.children.push(target); | |
hasParentFlag[target.id] = true; | |
} | |
}); | |
return root; | |
} | |
/** | |
通过cluster.nodes(tree),为tree的每个节点计算x,y,depth等属性以便定位 | |
*/ | |
var buildNodes = function (tree) { | |
return cluster.nodes(tree); | |
} | |
/** | |
建立节点之间各条边。 | |
如果直接调用cluster.links(nodes),其只支持树状结构,回路会被丢弃,借此把所有边补充完整。 | |
*/ | |
var buildLinks = function (data) { | |
var result = []; | |
self.traverseEdge(data, function (source, target, ref) { | |
result.push({ | |
'source': source, | |
'target': target, | |
'ref': ref | |
}); | |
}); | |
return result; | |
} | |
/** | |
更新数据时保留原有节点的位置信息 | |
*/ | |
var merge = function (nodes, links) { | |
var oldData = []; | |
if (self.nodes) { //原nodes存在,输出oldData | |
self.nodes.forEach(function (d) { | |
oldData[d.id] = d; | |
}); | |
} | |
if (oldData) { //用oldData里的数据覆盖现nodes里的数据 | |
nodes.forEach(function (d) { | |
if (oldData[d.id]) { | |
d.x = oldData[d.id].x; | |
d.y = oldData[d.id].y; | |
} | |
}); | |
} | |
self.nodes = nodes; | |
self.links = links; | |
} | |
//1)连通图->树 参见:https://github.com/mbostock/d3/wiki/Cluster-Layout) | |
//1)temporarily convert a connectivity to a tree | |
var tree = conv2tree(data); | |
//2)根据树状结构计算节点位置. | |
//2)caculate for nodes' coords with <code>cluster.nodes(tree);</code> | |
var nodes = buildNodes(tree); | |
//3)因为连通图是网状而非树状,将所有边补充完整 | |
//3)fill in all the edges(links) of the connectivity | |
var links = buildLinks(data); | |
//4)与原有的数据做一次merge,保留位置等信息 | |
//4)do merge to keep info like node's position | |
merge(nodes, links); | |
//5)重绘 | |
//5)redraw | |
self.redraw(); | |
} | |
/// call redraw() if necessary (reconfig,recostom the actions, etc. ) | |
this.redraw = function () { | |
var fontSize = 8 | |
var lineSpace = 2 | |
var boxHeight = 50 | |
var boxWidth = 85 | |
var width = self.config.bg_size.width; | |
var height = self.config.bg_size.height; | |
var yscale_performancebar = d3.scale.linear() | |
.domain([0, 1]) | |
.rangeRound([boxHeight / 2, -boxHeight / 2]) | |
var diagonal = d3.svg.diagonal() | |
.projection(function (d) { | |
return [d.y - boxWidth / 2, d.x]; | |
}); | |
var _clear = function () { | |
d3.select("svg").remove(); | |
svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.append("g") | |
.attr("transform", "translate(80,0)"); | |
svg.append("svg:defs").selectAll("marker") | |
.data(["suit"]) | |
.enter().append("svg:marker") | |
.attr("id", "idArrow") | |
.attr("viewBox", "0 -5 10 10") | |
.attr("refX", 15) | |
.attr("refY", -1.5) | |
.attr("markerWidth", 6) | |
.attr("markerHeight", 6) | |
.attr("orient", "auto") | |
.append("svg:path") | |
.attr("d", "M0,-5L10,0L0,5"); | |
} | |
var _redrawEdges = function () { | |
var linksWithArrow = self.links; | |
//to show arrow at the end of the path with fixed size, we have to copy each path with .stroke-width=1 | |
if (self.config.edge_show_arrow) { | |
linksWithArrow = []; | |
self.links.forEach(function (d) { | |
var fake = {}; | |
for (prop in d) { | |
fake[prop] = d[prop]; | |
} | |
fake.faked = true; //copy each path with .faked=true as flag | |
linksWithArrow.push(fake); | |
linksWithArrow.push(d); | |
}) | |
} | |
var path = svg.selectAll(".link").data(linksWithArrow); | |
// when new path arrives | |
path.enter().insert("path", ":first-child") | |
.attr("marker-end", function (d) { | |
if (d.faked) return "url(#idArrow)"; | |
}) | |
.attr("id", function (d) { | |
if (!d.faked) return "link" + d.ref.from + "-" + d.ref.to; | |
}) | |
.attr("class", function (d) { | |
return "link" + " link-" + d.ref.from + " link-" + d.ref.to; | |
}) | |
.attr("d", diagonal) | |
.transition() | |
.duration(1000) | |
.style("stroke-width", function (d) { | |
if (d.faked) { | |
return 1; | |
} | |
if (d.ref.edge_width) return Math.max(1, boxHeight / 2 * d.ref.edge_width); //won't become invisible if too thin | |
else return self.config.edge_def_width; //default value | |
}) | |
// when path changes | |
path.attr("d", diagonal) | |
// when path's removed | |
path.exit().remove(); | |
} | |
_clear(); | |
_redrawEdges(); | |
///show description on each path(edge) | |
if (showPathDesc) { | |
svg.selectAll(".abc").data(self.links).enter().append("text").append("textPath") | |
.attr("xlink:xlink:href", function (d) { | |
return "#link" + d.ref.from + "-" + d.ref.to; | |
}) //why not .attr("xlink:href",...)? this's a hack, see https://groups.google.com/forum/?fromgroups=#!topic/d3-js/vLgbiM4ki1g | |
.attr("startOffset", "50%") | |
.text(showPathDesc) | |
} | |
///show each node with text | |
var existingNodes = svg.selectAll(".node").data(self.nodes); //选中所有节点 | |
//矩形 | |
//draw rectangle | |
var newNodes = existingNodes.enter().append("g"); | |
newNodes.attr("class", "node") | |
.attr("id", function (d) { | |
return "node-" + d.id | |
}) | |
.attr("transform", function (d) { | |
return "translate(" + d.y + "," + d.x + ")"; | |
}) | |
//.append("rect") //make nodes as rectangles OR: | |
.append("circle").attr('r',50) //make nodes as circles | |
.attr('class', 'nodebox') | |
.attr("x", -boxWidth / 2) | |
.attr("y", -boxHeight / 2) | |
.attr("width", boxWidth) | |
.attr("height", boxHeight) | |
if (self.config.node_draggable) { | |
newNodes.call(d3.behavior.drag().origin(Object).on("drag", function (d) { | |
//拖动时移动节点 | |
//translate the node | |
function translate(x, y) { | |
return { | |
'x': x, | |
'y': y | |
} | |
} | |
var coord = eval(d3.select(this).attr("transform")); | |
d3.select(this) | |
.attr("transform", "translate(" + (coord.x + d3.event.dx) + "," + (coord.y + d3.event.dy) + ")") | |
//拖动时重绘边 | |
//update node's coord ,then redraw affected edges | |
d.x = d.x + d3.event.dy; | |
d.y = d.y + d3.event.dx; | |
_redrawEdges(); | |
})); | |
} | |
//红色柱状性能指示图 | |
//show performance bar | |
if (self.config.show_performance_bar) { | |
newNodes.append("rect") | |
.attr('class', 'performancebar') | |
.attr("x", boxWidth / 2 * 1.05) | |
.attr("width", boxWidth / 10) | |
.style("fill", "red") | |
.style("stroke", "red") | |
.attr("y", boxHeight / 2) | |
.attr("height", 0) | |
//计算柱状图高度 | |
existingNodes.select('.performancebar') | |
.transition() | |
.duration(1000) | |
.attr("y", function (d) { | |
return yscale_performancebar(d.load) | |
}) | |
.attr("height", function (d) { | |
return boxHeight / 2 - yscale_performancebar(d.load) | |
}) | |
} | |
///构造文案容器 | |
///text constructors | |
//标题 | |
//node titles | |
newNodes.append("text") | |
.attr("class", "nodeTitle") | |
.attr("y", -boxHeight / 2 + fontSize + 2 * lineSpace) | |
.attr("text-anchor", "middle") | |
//副标题 | |
//node subhead | |
newNodes.append("text") | |
.attr("text-anchor", "middle") | |
.attr("class", "nodeText f1Text") | |
.attr("y", -boxHeight / 2 + 2 * fontSize + 3 * lineSpace) | |
//详情矩阵 | |
//node body text | |
newNodes.append("g") | |
.attr("class", "confusionmatrix") | |
.selectAll("g").data(function (d) { | |
return d.confusionmatrix ? d.confusionmatrix : [] | |
}) | |
.enter().append("g") | |
.attr("class", "rows") | |
.attr("transform", function (d, i) { | |
return "translate(" + (-15) + "," + (-boxHeight / 2 + (i + 3) * fontSize + (i + 4) * lineSpace) + ")"; | |
}) | |
.selectAll("g").data(function (d) { | |
return d | |
}) | |
.enter().append("g") | |
.attr("class", "columns") | |
.attr("transform", function (d, i) { | |
return "translate(" + i * 30 + ",0)"; | |
}) | |
.append("text") | |
.attr("text-anchor", "middle") | |
.attr("class", "nodeText") | |
///显示文案 | |
///show text | |
existingNodes.select(".nodeTitle").text(showTitleAction ? showTitleAction : function (d) { | |
return d.id + ")" + d.name | |
}); //标题 | |
existingNodes.select(".f1Text").text(showSubheadAction ? showSubheadAction : function (d) { | |
return Math.round(d.load * 100) + "%" | |
}); //副标题 | |
existingNodes.select(".confusionmatrix") //详情矩阵 | |
.selectAll(".rows") | |
.data(function (d) { | |
return d.confusionmatrix ? d.confusionmatrix : [] | |
}) | |
.selectAll(".columns") //rows | |
.data(function (d) { | |
return d | |
}) | |
.select("text") | |
.text(function (d) { | |
return d | |
}) | |
} | |
/** | |
返回根节点 | |
return the root node | |
*/ | |
this.getRoot = function (data) { | |
return data['0']; | |
}; | |
/** | |
遍历所有节点 | |
traverse all nodes | |
callback(node) | |
*/ | |
this.traverse = function (data, callback) { | |
if (!data) console.error('data is null') | |
function _init() { | |
var i; | |
for (i in data) { | |
data[i].visited = false; | |
} | |
} | |
function _traverse(pt, callback) { | |
if (!pt) { | |
return; | |
} | |
pt.visited = true; | |
console.debug("traverse node:" + pt.id); | |
callback(pt); | |
if (pt.ref) { | |
pt.ref.forEach(function (ref) { | |
var childNode = data[ref.to.toString()]; | |
if (childNode && !childNode.visited) { | |
_traverse(childNode, callback); | |
} | |
}) | |
} | |
} | |
_init(); | |
_traverse(self.getRoot(data), callback); | |
}; | |
/** | |
遍历所有边 | |
traverse all edges | |
callback(sourceNode,targetNode,ref) | |
*/ | |
this.traverseEdge = function (data, callback) { | |
if (!data) console.error('data is null') | |
self.traverse(data, function (node) { | |
if (node.ref) { | |
node.ref.forEach(function (ref) { | |
var childNode = data[ref.to.toString()]; | |
if (childNode) { | |
console.debug("traverse edge:" + node.id + "-" + childNode.id); | |
callback(node, childNode, ref); | |
} | |
}); | |
} | |
}); | |
}; | |
} | |
/////////function(class) Graph end//////////////// | |
function init_page(){ | |
// request data here | |
var data = { | |
"0": { | |
id: 0, | |
name: "root", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 0, | |
to: 1, | |
edge_width: Math.random(), | |
}, { | |
from: 0, | |
to: 4, | |
edge_width: Math.random(), | |
}, { | |
from: 0, | |
to: 5, | |
edge_width: Math.random(), | |
}, { | |
from: 0, | |
to: 6, | |
edge_width: Math.random(), | |
}, { | |
from: 0, | |
to: 7, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"1": { | |
id: 1, | |
name: "systemA", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 1, | |
to: 2, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"2": { | |
id: 2, | |
name: "systemB", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 2, | |
to: 3, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"3": { | |
id: 3, | |
name: "systemC", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
}, | |
"4": { | |
id: 4, | |
name: "systemD", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 4, | |
to: 2, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"5": { | |
id: 5, | |
name: "systemE", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
}, | |
"6": { | |
id: 6, | |
name: "systemF", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 6, | |
to: 2, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"7": { | |
id: 7, | |
name: "systemG", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 7, | |
to: 3, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
}; | |
//customize anything here | |
myGraph.showTitle(function (d) { | |
return d.name; | |
}); | |
myGraph.showSubhead(function (d) { | |
return "p0"; | |
}); | |
myGraph.showPathDesc(function (d) { | |
return d.ref.edge_width.toFixed(2); | |
}); | |
myGraph.bind(data); | |
} | |
function refresh() { | |
//request data here | |
var data = { | |
"0": { | |
id: 0, | |
name: "systemA", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 0, | |
to: 1, | |
edge_width: Math.random(), | |
}, { | |
from: 0, | |
to: 4, | |
edge_width: Math.random(), | |
}, { | |
from: 0, | |
to: 5, | |
edge_width: Math.random(), | |
}, | |
/* | |
{ | |
from:0, | |
to:6, | |
edge_width:Math.random(), | |
}, | |
*/ | |
{ | |
from: 0, | |
to: 7, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"1": { | |
id: 1, | |
name: "systemB", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 1, | |
to: 2, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"2": { | |
id: 2, | |
name: "systemC", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 2, | |
to: 3, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"3": { | |
id: 3, | |
name: "systemD", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
}, | |
"4": { | |
id: 4, | |
name: "systemE", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 4, | |
to: 2, | |
edge_width: Math.random(), | |
}, { | |
from: 4, | |
to: 8, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"5": { | |
id: 5, | |
name: "systemF", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
}, | |
"8": { // change 6 to 8 | |
id: 8, | |
name: "systemI", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 8, | |
to: 7, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
"7": { | |
id: 7, | |
name: "systemH", | |
load: Math.random(), | |
confusionmatrix: [ | |
[7293, 1224], | |
[7293, 1224] | |
], | |
ref: [{ | |
from: 7, | |
to: 3, | |
edge_width: Math.random(), | |
}, ] | |
}, | |
}; | |
myGraph.bind(data); | |
} | |
var myGraph = new graph(d3); //http://d3js.org/ | |
init_page(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>hierarchical classification</title> | |
<link crossorigin="anonymous" href="nodes.css" media="all" rel="stylesheet" /> | |
</head> | |
<body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script> | |
<script src="hierarchical-classification.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.nodebox { | |
fill: #fff; | |
stroke: steelblue; | |
stroke-width: 3.5px; | |
} | |
#node_PHYSICAL .nodebox { | |
stroke: beige; | |
} | |
#node_PHYSICAL_HARD .nodebox { | |
stroke: tan; | |
} | |
#node_PHYSICAL_SOFT .nodebox { | |
stroke: yellow; | |
} | |
#node_BIOTA .nodebox { | |
stroke: darkgreen; | |
} | |
#node_BIOTA_SPONGES .nodebox { | |
stroke: orange; | |
} | |
#node_BIOTA_ALGAE .nodebox { | |
stroke: green; | |
} | |
#node_BIOTA_ALGAE_CANOPY_ECK .nodebox { | |
stroke: lightgreen; | |
} | |
#node_BIOTA_ALGAE_CRUSTOSE .nodebox { | |
stroke: purple; | |
} | |
.nodeTitle { | |
font: 8px sans-serif; | |
font-weight: bold; | |
} | |
.nodeText { | |
font: 8px sans-serif; | |
} | |
.f1Text { | |
fill: orange; | |
font-weight: bold; | |
} | |
.link { | |
fill: none; | |
stroke: #ccc; | |
stroke-width: 3.5px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment