【问题标题】:Layout geometry for tree structures / laying out nodes attractively树结构的布局几何/有吸引力的节点布局
【发布时间】:2011-06-05 17:27:25
【问题描述】:

我有一个树状数据结构,我想在 SVG 画布上绘制它(使用 jQuery SVG)。我想渲染节点以吸引人的间距方式从上到下展开。

理想情况下,我需要一个类或函数,我可以将树的表示传递给它并返回每个节点的 X 和 Y 坐标。不幸的是,我的谷歌搜索被列表树 GUI 小部件的数千次点击感到沮丧,这不是我想要的。

理想情况下,这将是一些 Javascript,但我也可以使用一些 Python(在服务器端使用。)我在 Github 上查看了 this Python 的东西,但我真的不能做头或尾它的用法(或让它运行。)我还简要介绍了ete2a1,但它未能正确安装,文档未完成,而且它似乎更适合实际将树渲染为图像而不是做几何。

抱歉,如果这是一个模糊的问题,请告诉我是否可以更清楚地说明。基本上,我有这个:

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

(呃,这可能不太正确,但你明白了。)

非常感谢任何提示!

【问题讨论】:

  • 你考虑过Dot吗? (有 Python 绑定:code.google.com/p/pydot)。
  • @Oli Charlesworth - 谢谢,我会看看。我希望 GraphViz 有一个更轻量级的替代品,但这看起来确实是一个潜在的解决方案。
  • 这是我想从 python 中做的事情,希望其他人对此有一些经验。
  • @shuttle87:正如 Oli Charlesworth 建议的那样,我最终将 Dot 与 pydot 绑定一起使用。但是,这从来都不是一个特别方便的解决方案,因为(至少在两年前)pydot 没有出色的文档记录,并且似乎仍然需要写出 Dot 文件并再次读取它们以获取节点位置属性。如果有人有更好的解决方案,我会留下这个问题。

标签: javascript python tree drawing


【解决方案1】:

强烈推荐 d3.js。这个collapsible dendrogram 非常适合大型数据集。还有一个regular dendrogram。非常容易即插即用,尤其是 jQuery 背景。

【讨论】:

    【解决方案2】:

    我前段时间研究过这个问题,并在 javascript 中挖掘了我的旧实现。

    这假设所有节点都具有相同的宽度,但它可以很好地推广到每个节点都包含自己的自定义宽度的情况。只需查找 node_size 引用并进行相应的概括。

    以下是嵌入了 javascript 的完整 HTML 文件。刷新浏览器会生成一个新的随机树并绘制它。但是你可以忽略我使用旧的 CSS 技巧来渲染框和线的绘图例程。坐标存储在树节点中。

    这是一个示例输出。它显示了唯一的显着弱点。这是“左贪婪”。如果像根为 60 的子树可以左右滑动,那么它总是尽可能地向左。一个特殊目的的 hack 解决了同一父节点的非叶子之间的叶子的问题,例如73. 通用解决方案比较棘手。

    <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>
    

    【讨论】:

      【解决方案3】:

      我将在这里根据我两年前一起破解的 Python 提供一个答案,尽管它远非一个好的答案。正如 Oli Charlesworth 建议的那样,我通过 PyDot 使用了 Graphviz。完整的脚本发布在下面,但首先我必须进行设置才能使其工作(假设您使用的是 virtualenv):

      首先,确保已安装 graphviz。假设您使用 apt 的东西:

      $> sudo apt-get install graphviz
      

      使用 virtualenv 包装器创建一个新的 virtualenv:

      $> mkvirtualenv pydot
      

      在新的 virtualenv 中,将 pyparsing 版本固定到 2.0 之前的版本(dot_parser 工作所必需的):

      (pydot)$> pip install pyparsing==1.5.7
      

      安装 pydot:

      (pydot)$> pip install pydot
      

      现在我使用的 Python(警告:它不是好):

      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))
      

      虽然这很丑陋,但它或多或少地符合我的目的。如果有人能找到一个更好的解决方案,我会很感兴趣。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-09-20
        • 2014-06-23
        • 1970-01-01
        • 1970-01-01
        • 2014-02-09
        • 1970-01-01
        • 1970-01-01
        • 2014-10-27
        相关资源
        最近更新 更多