Javascript 树结构的布局几何图形/布局节点的吸引力
我有一个树状的数据结构,我想在SVG画布上绘制它(使用jQuery SVG)。我想以一种吸引人的方式呈现从上到下展开的节点 理想情况下,我需要一个类或函数,我可以将树的表示形式传递给它,并返回每个节点的X和Y坐标。不幸的是,我的谷歌搜索被成千上万的列表树GUI小部件的点击所挫败,这不是我想要的 理想情况下,这将是一些Javascript,但我也可以使用一些Python(在服务器端使用)。我在Github上查看了Python的内容,但我真的无法了解其使用情况(或让它运行)。我也简要查看了一下,但它没有正确安装,文档尚未完成,而且它似乎更适合于将树实际渲染为图像,而不是进行几何处理 抱歉,如果这是一个模糊的问题,请让我知道,如果我可以更清楚。基本上,我有:Javascript 树结构的布局几何图形/布局节点的吸引力,javascript,python,tree,drawing,Javascript,Python,Tree,Drawing,我有一个树状的数据结构,我想在SVG画布上绘制它(使用jQuery SVG)。我想以一种吸引人的方式呈现从上到下展开的节点 理想情况下,我需要一个类或函数,我可以将树的表示形式传递给它,并返回每个节点的X和Y坐标。不幸的是,我的谷歌搜索被成千上万的列表树GUI小部件的点击所挫败,这不是我想要的 理想情况下,这将是一些Javascript,但我也可以使用一些Python(在服务器端使用)。我在Github上查看了Python的内容,但我真的无法了解其使用情况(或让它运行)。我也简要查看了一下,但它
Tree("Root",
Tree("leaf 1",
Tree("leaf 2",
Tree("leaf 4"),
Tree("leaf 5")), Tree("leaf 3",
Tree("leaf 6"))))
这将导致:
Root
|
leaf 1
|
/\
/ \
leaf 2 leaf 3
/\ \
/ \ \
leaf 4 leaf 5 leaf 6
(呃,这可能不太正确,但你明白了。)
非常感谢任何提示 这里,我将根据两年前我共同开发的Python提供一个答案,尽管这远不是一个好答案。正如奥利·查尔斯沃思建议的那样,我通过PyDot使用Graphviz。完整的脚本发布在下面,但首先我必须进行设置才能使其正常工作(假设您使用的是virtualenv): 首先,确保安装了graphviz。假设您正在使用apt:
$> sudo apt-get install graphviz
使用virtualenv包装器创建新的virtualenv:
$> mkvirtualenv pydot
在新的virtualenv中,将pyparsing版本固定到2.0之前的版本(dot_解析器工作所需):
安装pydot:
(pydot)$> pip install pydot
现在我使用的Python(警告:它不好):
导入操作系统
进口稀土
导入系统
导入临时文件
导入pydot
测试图={
“根”:{
“投入”:[],
},
“1”:{
“输入”:[“根”],
},
“第2条”:{
“输入”:[“Leaf1”],
},
“第3条”:{
“输入”:[“Leaf1”],
},
“第4条”:{
“输入”:[“Leaf2”],
},
“第5条”:{
“输入”:[“Leaf2”],
},
“第6条”:{
“输入”:[“Leaf3”],
},
}
类DotError(StandardError):
“点问题。”
# http://www.graphviz.org/doc/info/lang.html
原始名称=r“(^[A-Za-z][A-Za-Z0-9.]*$)|(^-?([0-9]+[0-9]+[0-9]+([0-9]*))$)
def条件报价(名称):
“”“如果名称与上面的正则表达式匹配,则Dot将其引为引号,否则不会”“”
如果re.match(原始名称)为无:
返回“\%s\”“%name”
返回名称
def get_node_位置(nodedict,aspect=None):
“”“构建pydot图,输出名为->[x,y]的字典”“”
#根据需要调整定位参数:
g=pydot.Dot(margin=“0.1”,ranksep=“0.7”,nodesep=“1.5”)
如果方面不是无:
g、 设置方向(圆形(方向))
nodedict.items()中的节点作为名称:
n=pydot.Node(名称,width=“0.5”,fixedsize=“0.5”)
g、 添加_节点(n)
nodedict.items()中的节点作为名称:
对于节点[“输入”]中的i:
尝试:
src=g.get_节点(条件(i))
如果存在(src,列表):
src=src[0]
dst=g.get_节点(条件(名称))
如果存在(dst,列表):
dst=dst[0]
g、 添加_边(pydot.edge(src,dst))
除索引器外:
打印“未找到输入%s”%i
#dot在<4个节点上似乎不起作用…请防止
#通过抛出一个错误
如果len(nodedict)<4:
raise DOTTERROR(“少于4个节点的点打断”)
#现在特别可怕的一点是把脚本写成点文件,
#然后再次阅读并提取位置。
#警告:当前未清理临时文件。
将tempfile.NamedTemporaryFile(delete=False,后缀=“.dot”)作为t:
t、 关闭()
g、 书写点(t.name)
g=pydot.graph_,来自_dot_文件(t.name)
out={}
nodedict.items()中的节点作为名称:
gn=g.get_节点(条件(名称))
如果存在(gn,列表):
gn=gn[0]
out[name]=[int(d)\
对于gn.get_pos()中的d。替换(“,”).split(“,”)]
返回
如果名称=“\uuuuu main\uuuuuuuu”:
打印(获取节点位置(测试图))
虽然这很难看,但它或多或少满足了我的目的。如果有人能找到更好的解决方案,我会很感兴趣的。I高度推荐d3.js。这对于大型数据集来说是非常好的。还有一个。非常容易即插即用的解决方案,特别是在jQuery背景下。我前一段时间做过这项工作,并且ug用javascript实现了我的旧实现 这假设所有节点都具有相同的宽度,但它可以很好地推广到每个节点都包含自己的自定义宽度的情况。只需查找
node\u size
引用并相应地进行推广
下面是一个嵌入javascript的完整HTML文件。刷新浏览器会生成一个新的随机树并绘制它。但是您可以忽略我的绘制例程,这些例程使用旧的CSS技巧来渲染框和线。坐标存储在树节点中
这是一个输出示例。它显示了唯一明显的弱点。它是“左贪婪”的。在60的子树可以左右滑动的地方,它总是尽可能地向左滑动。一个特殊用途的hack解决了相同父树的非叶之间的叶的问题,例如73。一般的解决方案更复杂。
.节点{
位置:绝对位置;
背景色:#0000cc;
颜色:#ffffff;
字体大小:12px;
字体系列:无衬线;
文本对齐:居中;
垂直对齐:中间对齐;
边框:1px实心#000088;
}
多特先生{
位置:绝对位置;
背景色:黑色;
宽度:1px;
高度:1px;
溢出:隐藏
(pydot)$> pip install pydot
import os
import re
import sys
import tempfile
import pydot
TEST_GRAPH = {
"Root" : {
"inputs" : [],
},
"Leaf1" : {
"inputs" : ["Root"],
},
"Leaf2" : {
"inputs" : ["Leaf1"],
},
"Leaf3" : {
"inputs" : ["Leaf1"],
},
"Leaf4" : {
"inputs" : ["Leaf2"],
},
"Leaf5" : {
"inputs" : ["Leaf2"],
},
"Leaf6" : {
"inputs" : ["Leaf3"],
},
}
class DotError(StandardError):
"""Dot problems."""
# http://www.graphviz.org/doc/info/lang.html
RAW_NAME_RE = r"(^[A-Za-z_][a-zA-Z0-9_]*$)|(^-?([.[0-9]+|[0-9]+(.[0-9]*)?)$)"
def conditional_quote(name):
"""Dot quotes names if they match the regex above, otherwise not"""
if re.match(RAW_NAME_RE, name) is None:
return "\"%s\"" % name
return name
def get_node_positions(nodedict, aspect=None):
"""Build the pydot graph, outputting a dictionary of name -> [x,y]"""
# Tweak positioning parameters as required:
g = pydot.Dot(margin="0.1", ranksep="0.7", nodesep="1.5")
if aspect is not None:
g.set_aspect(round(aspect))
for name, node in nodedict.items():
n = pydot.Node(name, width="0.5", fixedsize="0.5")
g.add_node(n)
for name, node in nodedict.items():
for i in node["inputs"]:
try:
src = g.get_node(conditional_quote(i))
if isinstance(src, list):
src = src[0]
dst = g.get_node(conditional_quote(name))
if isinstance(dst, list):
dst = dst[0]
g.add_edge(pydot.Edge(src, dst))
except IndexError:
print "Input %s not found" % i
# dot doesn't seem to work on < 4 nodes... prevent it from
# by just throwing an error
if len(nodedict) < 4:
raise DotError("Dot breaks with less than 4 nodes.")
# Now the particularly horrible bit. Write the script as a dot file,
# then read it in again and extract the positionings.
# WARNING: Currently doesn't clean up the temp file.
with tempfile.NamedTemporaryFile(delete=False, suffix=".dot") as t:
t.close()
g.write_dot(t.name)
g = pydot.graph_from_dot_file(t.name)
out = {}
for name, node in nodedict.items():
gn = g.get_node(conditional_quote(name))
if isinstance(gn, list):
gn = gn[0]
out[name] = [int(d) \
for d in gn.get_pos().replace('"', "").split(",")]
return out
if __name__ == "__main__":
print(get_node_positions(TEST_GRAPH))
<html>
<head>
<style>
.node {
position: absolute;
background-color: #0000cc;
color: #ffffff;
font-size: 12px;
font-family: sans-serif;
text-align: center;
vertical-align: middle;
border: 1px solid #000088;
}
.dot {
position: absolute;
background-color: black;
width: 1px;
height: 1px;
overflow:hidden;
}
</style>
<script>
var node_size = 18
var horizontal_gap = 16
var vertical_gap = 32
// Draw a graph node.
function node(lbl, x, y, sz) {
if (!sz) sz = node_size
var h = sz / 2
document.write('<div class="node" style="left:' + (x - h) + 'px;top:' + (y - h) +
'px;width:' + sz + 'px;height:' + sz + 'px;line-height:' + sz +
'px;">' + lbl + '</div>')
}
// Draw a 1-pixel black dot.
function dot(x, y) {
document.write('<div class="dot" style="left:' + x + 'px;top:' + y + 'px;"></div>')
}
// Draw a line between two points. Slow but sure...
function arc(x0, y0, x1, y1) {
var dx = x1 - x0
var dy = y1 - y0
var x = x0
var y = y0
if (abs(dx) > abs(dy)) {
var yinc = dy / dx
if (dx < 0)
while (x >= x1) { dot(x, y); --x; y -= yinc }
else
while (x <= x1) { dot(x, y); ++x; y += yinc }
}
else {
var xinc = dx / dy
if (dy < 0)
while (y >= y1) { dot(x, y); --y; x -= xinc }
else
while (y <= y1) { dot(x, y); ++y; x += xinc }
}
}
// Tree node.
function Tree(lbl, children) {
this.lbl = lbl
this.children = children ? children : []
// This will be filled with the x-offset of this node wrt its parent.
this.offset = 0
// Optional coordinates that can be written by place(x, y)
this.x = 0
this.y = 0
}
Tree.prototype.is_leaf = function() { return this.children.length == 0 }
// Label the tree with given root (x,y) coordinates using the offset
// information created by extent().
Tree.prototype.place = function(x, y) {
var n_children = this.children.length
var y_children = y + vertical_gap + node_size
for (var i = 0; i < n_children; i++) {
var child = this.children[i]
child.place(x + child.offset, y_children)
}
this.x = x
this.y = y
}
// Draw the tree after it has been labeled w ith extent() and place().
Tree.prototype.draw = function () {
var n_children = this.children.length
for (var i = 0; i < n_children; i++) {
var child = this.children[i]
arc(this.x, this.y + 0.5 * node_size + 2, child.x, child.y - 0.5 * node_size)
child.draw()
}
node(this.lbl, this.x, this.y)
}
// Recursively assign offsets to subtrees and return an extent
// that gives the shape of this tree.
//
// An extent is an array of left-right x-coordinate ranges,
// one element per tree level. The root of the tree is at
// the origin of its coordinate system.
//
// We merge successive extents by finding the minimum shift
// to the right that will cause the extent being merged to
// not overlap any of the previous ones.
Tree.prototype.extent = function() {
var n_children = this.children.length
// Get the extents of the children
var child_extents = []
for (var i = 0; i < n_children; i++)
child_extents.push(this.children[i].extent())
// Compute a minimum non-overlapping x-offset for each extent
var rightmost = []
var offset = 0
for (i = 0; i < n_children; i++) {
var ext = child_extents[i]
// Find the necessary offset.
offset = 0
for (var j = 0; j < min(ext.length, rightmost.length); j++)
offset = max(offset, rightmost[j] - ext[j][0] + horizontal_gap)
// Update rightmost
for (var j = 0; j < ext.length; j++)
if (j < rightmost.length)
rightmost[j] = offset + ext[j][1]
else
rightmost.push(offset + ext[j][1])
this.children[i].offset = offset
}
rightmost = null // Gc, come get it.
// Center leaves between non-leaf siblings with a tiny state machine.
// This is optional, but eliminates a minor leftward skew in appearance.
var state = 0
var i0 = 0
for (i = 0; i < n_children; i++) {
if (state == 0) {
state = this.children[i].is_leaf() ? 3 : 1
} else if (state == 1) {
if (this.children[i].is_leaf()) {
state = 2
i0 = i - 1 // Found leaf after non-leaf. Remember the non-leaf.
}
} else if (state == 2) {
if (!this.children[i].is_leaf()) {
state = 1 // Found matching non-leaf. Reposition the leaves between.
var dofs = (this.children[i].offset - this.children[i0].offset) / (i - i0)
offset = this.children[i0].offset
for (j = i0 + 1; j < i; j++)
this.children[j].offset = (offset += dofs)
}
} else {
if (!this.children[i].is_leaf()) state = 1
}
}
// Adjust to center the root on its children
for (i = 0; i < n_children; i++)
this.children[i].offset -= 0.5 * offset
// Merge the offset extents of the children into one for this tree
var rtn = [ [-0.5 * node_size, 0.5 * node_size] ]
// Keep track of subtrees currently on left and right edges.
var lft = 0
var rgt = n_children - 1
i = 0
for (i = 0; lft <= rgt; i++) {
while (lft <= rgt && i >= child_extents[lft].length) ++lft
while (lft <= rgt && i >= child_extents[rgt].length) --rgt
if (lft > rgt) break
var x0 = child_extents[lft][i][0] + this.children[lft].offset
var x1 = child_extents[rgt][i][1] + this.children[rgt].offset
rtn.push([x0, x1])
}
return rtn
}
// Return what the bounding box for the tree would be if it were drawn at (0,0).
// To place it at the upper left corner of a div, draw at (-bb[0], -bb[1])
// The box is given as [x_left, y_top, width, height]
function bounding_box(extent) {
var x0 = extent[0][0]
var x1 = extent[0][1]
for (var i = 1; i < extent.length; i++) {
x0 = min(x0, extent[i][0])
x1 = max(x1, extent[i][1])
}
return [x0, -0.5 * node_size, x1 - x0, (node_size + vertical_gap) * extent.length - vertical_gap ]
}
function min(x, y) { return x < y ? x : y }
function max(x, y) { return x > y ? x : y }
function abs(x) { return x < 0 ? -x : x }
// Generate a random tree with given depth and minimum number of children of the root.
// The min_children field is optional. Use e.g. 2 to avoid trivial trees.
var node_label = 0
function random_tree(depth, min_children) {
var n_children = depth <= 1 || Math.random() < 0.5 ? 0 : Math.round(Math.random() * 4)
if (min_children) n_children = max(n_children, min_children)
var children = []
for (var i = 0; i < n_children; i++)
children.push(random_tree(depth - 1, min_children - 1))
return new Tree('' + node_label++, children)
}
</script>
</head>
<body>
<div style="width:1000px;height:800px">
<script>
// Generate a random tree.
tree = random_tree(10, 2)
// Label it with node offsets and get its extent.
e = tree.extent()
// Retrieve a bounding box [x,y,width,height] from the extent.
bb = bounding_box(e)
// Label each node with its (x,y) coordinate placing root at given location.
tree.place(-bb[0] + horizontal_gap, -bb[1] + horizontal_gap)
// Draw using the labels.
tree.draw()
</script>
</div>
</body>
</html>