4

Background: I have some data that I want to present in a table. Each column in the table has a header. Some of these headers in turn, have common headers. In other words, I have a tree of headers that I want to show in the table cells.

Problem: How to nicely lay out a tree in the form of table by means of merging cells (cf merged cells in Excel or rowspan / colspan in HTML tables)?

Some requirements:

Given a tree like the following:

+ Header 1
|---+ Header 2
|   |---- Header 4
|   '---+ Header 5
|       |---- Header 8
|       '---- Header 9
'---+ Header 3
    |---- Header 6
    '---- Header 7
  • The resulting table should always be rectangular, i.e. this is not acceptable:

    .-------------------------------------------.
    |                  Header 1                 |
    +--------------------------+----------------+
    |    Header 2              |    Header 3    |
    +----------+---------------+--------+-------+
    | Header 4 |   Header 5    |  Hdr 6 | Hdr 7 |
    '----------+-------+-------+--------+-------'
               | Hdr 8 | Hdr 9 |
               '-------+-------'
    
  • The height of the cells should be as evenly distributed as possible. (There should be no unecessary height constraints between siblings.) For instance solving the above situation by simply letting the leaves grow downwards like this is not acceptable:

    .-------------------------------------------.
    |                  Header 1                 |
    +--------------------------+----------------+
    |    Header 2              |    Header 3    |
    +----------+---------------+--------+-------+  <-- Height of Header 3
    |          |   Header 5    |        |       |      constrained by
    | Header 4 +-------+-------+  Hdr 6 | Hdr 7 |      height of Header 2
    |          | Hdr 8 | Hdr 9 |        |       |
    '----------+-------+-------+--------+-------'
    
  • The correct output should look something like this:

    .-------------------------------------------.
    |                  Header 1                 |
    +--------------------------+----------------+
    |    Header 2              |    Header 3    |
    +----------+---------------+                |
    | Header 4 |   Header 5    |--------+-------+  <-- constraint relaxed.
    |          +-------+-------+  Hdr 6 | Hdr 7 |
    |          | Hdr 8 | Hdr 9 |        |       |
    '----------+-------+-------+--------+-------'
    
  • The number of rows used should be minimized. In other words, the right version is preferred over the left version below:

    .--------------------------.         .--------------------------.
    |  Rowspan 3               |         |  Rowspan 1               |
    +-------------+------------+         +-------------+------------+
    |  Rowspan 4  | Rowspan 6  |   -->   |  Rowspan 2  | Rowspan 3  |
    +-------------+            |         +-------------+            |
    |  Rowspan 6  +------------+         |  Rowspan 3  +------------+
    |             | Rowspan 4  |         |             | Rowspan 2  |
    '-------------+------------'         '-------------+------------'
    
    Unecessarily large rowspans.             Minimized rowspans.
      (actual height: 13 rows)             (actual height: 6 rows)
    
4

2 回答 2

8

像往常一样,仔细解释问题似乎有所帮助。我相信我想通了。关键思想是递归地遍历树并(除其他外)计算每一步中所有子树深度的最小公倍数

答案是用 Java 编写的,但重写为 PHP、C# 或 what-have-you 应该很简单。它指的是以下两个辅助类并以 HTML 表为目标。

class Tree {
    String val;
    Tree[] children;
    ...
}

class Cell {
    String val;
    int row, col, rowspan, colspan;
    ...
}

解决方案分为两部分:

  1. 从 转换TreeList<Cell>

  2. 布局List<Cell>合理<table>...</table>

    (如果以电子表格等为目标,则可能不需要。)

Tree到转换List<Cell>

这是使用下面定义的方法完成rowsToUsegetCells。前者计算布局给定树所需的总行数,后者生成实际Cell的 s。参数表示以下内容:

  • tCell是应该为其生成 s的树的根。
  • rowandcol表示最顶层(根)单元格的当前行和列。
  • rowsLeft指定当前树应该分布在多少行上。

以下是两种方法:

public static int rowsToUse(Tree t) {
    int childrenRows = t.children.length == 0 ? 0 : 1;
    for (Tree child : t.children)
        childrenRows = lcm(childrenRows, rowsToUse(child));
    return 1 + childrenRows;
}


public static List<Cell> getCells(Tree t, int row, int col, int rowsLeft) {

    // Add top-most cell corresponding to the root of the current tree.
    int rootRows = rowsLeft / rowsToUse(t);
    List<Cell> cells = new ArrayList<Cell>();
    cells.add(new Cell(t.val, row, col, rootRows, width(t)));

    // Generate cells for subtrees.
    for (Tree child : t.children) {
        cells.addAll(getCells(child, row+rootRows, col, rowsLeft-rootRows));
        col += width(child);
    }

    return cells;
}

方法depthwidthlcm是直截了当的。如果您愿意,请参阅底部的完整源代码。

布局List<Cell>合理<table>...</table>

public static String getHtmlTable(List<Cell> cells) {

    // Sort the cells primarily on row, secondarily on column.
    Collections.sort(cells, new Comparator<Cell>() {
        public int compare(Cell c1, Cell c2) {
            int pri = Integer.valueOf(c1.row).compareTo(c2.row);
            int sec = Integer.valueOf(c1.col).compareTo(c2.col);
            return pri != 0 ? pri : sec;
        }
    });

    // Lay out the cells row by row.
    StringBuilder result = new StringBuilder("<table><tbody>");
    for (int row = 0, i = 0; i < cells.size(); row++) {
        result.append("<tr>\n");
        for (; i < cells.size() && cells.get(i).row == row; i++)
            result.append(cells.get(i).asTdTag());
        result.append("</tr>\n");
    }
    return result.append("</tbody></table>").toString();
}

完整的源代码和演示。

这是完整的来源。鉴于树

Tree t = new Tree("1",
           new Tree("2",
             new Tree("4"),
               new Tree("5",
                 new Tree("8"),
                 new Tree("9"))),
           new Tree("3",
             new Tree("6"),
             new Tree("7")));

它产生以下表格主体:

<tr><td colspan='5'>1</td></tr>
<tr><td colspan='3' rowspan='2'>2</td><td colspan='2' rowspan='3'>3</td></tr>
<tr></tr>
<tr><td rowspan='4'>4</td><td colspan='2' rowspan='2'>5</td></tr>
<tr><td rowspan='3'>6</td><td rowspan='3'>7</td></tr>
<tr><td rowspan='2'>8</td><td rowspan='2'>9</td></tr>

看起来像

在此处输入图像描述

完整来源:

import java.io.*;
import java.util.*;

class Tree {

    String val;
    Tree[] children;

    public Tree(String val, Tree... children) {
        this.val = val;
        this.children = children;
    }
}


class Cell {
    String val;
    int row, col, rowspan, colspan;
    public Cell(String val, int row, int col, int rowspan, int colspan) {
        this.val = val;
        this.row = row;
        this.col = col;
        this.rowspan = rowspan;
        this.colspan = colspan;
    }

    public String asTdTag() {
        String cs = colspan == 1 ? "" : " colspan='" + colspan + "'";
        String rs = rowspan == 1 ? "" : " rowspan='" + rowspan + "'";
        return "<td" + cs + rs + ">" + val + "</td>";
    }
}


public class TreeTest {

    public static int rowsToUse(Tree t) {
        int childrenRows = t.children.length == 0 ? 0 : 1;
        for (Tree child : t.children)
            childrenRows = lcm(childrenRows, rowsToUse(child));
        return 1 + childrenRows;
    }


    public static List<Cell> getCells(Tree t, int row, int col, int rowsLeft) {

        // Add top-most cell corresponding to the root of the current tree.
        int rootRows = rowsLeft / rowsToUse(t);
        List<Cell> cells = new ArrayList<Cell>();
        cells.add(new Cell(t.val, row, col, rootRows, width(t)));

        // Generate cells for subtrees.
        for (Tree child : t.children) {
            cells.addAll(getCells(child, row+rootRows, col, rowsLeft-rootRows));
            col += width(child);
        }

        return cells;
    }


    public static int width(Tree t) {
        if (t.children.length == 0)
            return 1;
        int w = 0;
        for (Tree child : t.children)
            w += width(child);
        return w;
    }


    public static int lcm(int a, int b) {
        int c = a * b;
        while (b > 0) {
            int t = b;
            b = a % b;
            a = t;
        }
        return c / a;
    }


    public static String getHtmlTable(List<Cell> cells) {

        // Sort the cells primarily on row, secondarily on column.
        Collections.sort(cells, new Comparator<Cell>() {
            public int compare(Cell c1, Cell c2) {
                int pri = Integer.valueOf(c1.row).compareTo(c2.row);
                int sec = Integer.valueOf(c1.col).compareTo(c2.col);
                return pri != 0 ? pri : sec;
            }
        });

        // Lay out the cells row by row.
        StringBuilder result = new StringBuilder("<table><tbody>");
        for (int row = 0, i = 0; i < cells.size(); row++) {
            result.append("<tr>\n");
            for (; i < cells.size() && cells.get(i).row == row; i++)
                result.append("  " + cells.get(i).asTdTag() + "\n");
            result.append("</tr>\n");
        }
        return result.append("</tbody></table>").toString();
    }


    public static void main(String[] args) throws IOException {

        Tree t = new Tree("1",
                new Tree("2",
                  new Tree("4"),
                    new Tree("5",
                      new Tree("8"),
                      new Tree("9"))),
                new Tree("3",
                  new Tree("6"),
                  new Tree("7")));

        FileWriter fw = new FileWriter("tree.html");

        List<Cell> cells = getCells(t, 0, 0, rowsToUse(t));

        fw.write("<html><head><style>table, td { border-style: solid; } " +
                 "table { border-spacing: 0px; border-width: 0 0 1px 5px; } " +
                 "td { padding: 15px; text-align: center; " +
                 "border-width: 1px 5px 0 0;} </style></head><body>");
        fw.write(getHtmlTable(cells));
        fw.write("</body></html>");

        fw.close();
    }
}
于 2012-08-20T09:49:53.187 回答
0

在客户端执行此操作的一种可能方法是使用带有一些递归的 JavaScript。下面的代码假定树表示为一个邻接列表——一个二维数组,其中每个元素都包含一个子数组。以下是您帖子中示例树的定义:(var adjList = [[1, 2], [3, 4], [5, 6], [], [7, 8], [], [], [], []];请注意,索引已更改为基于 0 的索引,因此 1 变为 0,2 变为 1,等等...)。该代码非常不言自明(如果它含糊不清,我很乐意解释任何部分):

function fill(par, id) {
    var children = adjList[id].length;
    if(children == 0) {
        par.innerHTML += "Header " + (id + 1);
        return;
    }
    par.innerHTML += "<table border = '1px'><tr><td colspan = '" + children + "'>Header " + (id + 1) + "</td></tr><tr></tr></table>";
    var tbl = par.getElementsByTagName("table")[0];
    var rw = tbl.rows[1];
    for(var i = 0; i < children; i ++) {
        rw.innerHTML += "<td></td>";
    }
    for(var i = 0; i < children; i ++ ) {
        fill(tbl.rows[1].cells[i], adjList[id][i]);
    }
}

初始调用:fill(document.getElementsByTagName("body")[0], 0);

需要一些 CSS:

table {
     height: 100%;
}
td, tr, table {
    border-collapse: collapse;
}

当然,还有一个小演示:little link

结果将如下所示:

在此处输入图像描述

希望以任何方式有所帮助!

于 2012-08-20T10:27:21.440 回答