简体   繁体   中英

Generating horizontal and vertical family tree table with PHP

I'm developing a family tree kind of application that allows descendants to be added under parents that can go deep and wide as it can. I have the database perfectly constructed which is not the problem here. The problem I am having is to generate the table in HTML.

Example data structure:

Array
(
    [1] => Array
        (
            [name] => Igor
            [children] => 2
            [siblings] => 0
            [level] => 1
            [descendants] => Array
                (
                    [7] => Array
                        (
                            [name] => Rapid
                            [children] => 2
                            [siblings] => 1
                            [level] => 2
                            [descendants] => Array
                                (
                                    [8] => Array
                                        (
                                            [name] => Hodor
                                            [children] => 1
                                            [siblings] => 1
                                            [level] => 3
                                            [descendants] => Array
                                                (
                                                    [9] => Array
                                                        (
                                                            [name] => Hodor II
                                                            [children] => 1
                                                            [siblings] => 0
                                                            [level] => 4
                                                            [descendants] => Array
                                                                (
                                                                    [20] => Array
                                                                        (
                                                                            [name] => Hodor III
                                                                            [children] => 0
                                                                            [siblings] => 0
                                                                            [level] => 5
                                                                        )

                                                                )

                                                        )

                                                )

                                        )

                                    [14] => Array
                                        (
                                            [name] => Rapid II
                                            [children] => 0
                                            [siblings] => 1
                                            [level] => 3
                                        )

                                )

                        )

                    [2] => Array
                        (
                            [name] => Thunder
                            [children] => 0
                            [siblings] => 1
                            [level] => 2
                        )

                )

        )

)

The numeric array keys are the IDs of the person.

Desired output for horizontal table:

在此处输入图片说明

Vertical:

在此处输入图片说明

I'm not sure what is the best approach for recursively looping over the data while keeping account of the rowspans and colspans. How can I do this effectively with any kind of depth?

I think this piece of code should do the trick, it works with your sample data & some other sets I tried. Here's a screenshot of the output:

生成的家谱的屏幕截图。

Further explanations of how all parts work are in the comments.

<?php

$tree = [
    [
        'name'        => 'Igor',
        'children'    => 2,
        'siblings'    => 0,
        'level'       => 1,
        'descendants' => [
            [
                'name'        => 'Rapid',
                'children'    => 2,
                'siblings'    => 1,
                'level'       => 2,
                'descendants' => [
                    [
                        'name'        => 'Hodor',
                        'children'    => 1,
                        'siblings'    => 1,
                        'level'       => 3,
                        'descendants' => [
                            [
                                'name'        => 'Hodor II',
                                'children'    => 1,
                                'siblings'    => 0,
                                'level'       => 4,
                                'descendants' => [
                                    [
                                        'name'     => 'Hodor III',
                                        'children' => 0,
                                        'siblings' => 0,
                                        'level'    => 5
                                    ]
                                ]
                            ]
                        ]
                    ],
                    [
                        'name'     => 'Rapid II',
                        'children' => 0,
                        'siblings' => 1,
                        'level'    => 3
                    ]
                ]
            ],
            [
                'name'     => 'Thunder',
                'children' => 0,
                'siblings' => 1,
                'level'    => 2
            ]
        ]
    ]
];

// Loop over the tree. Every person in the root of the tree
// gets his own table(s).
foreach ($tree as $person) {
    $rows = [];
    parsePerson($person, $rows);

    $rows = cleanupRows($rows);

    output($rows);
    $rows = convertRowsToHorizontal($rows);
    output($rows);
}

/**
 * Convert a person in the tree to an array to be used to print the tables.
 *
 * @param array $person
 * @param array $rows
 * @param int   $level
 * @param int   $position
 *
 * @return int
 */
function parsePerson($person, &$rows, $level = 0, $position = 0)
{
    if (!empty($person['descendants'])) {
        // The colspan of this row is the sum of the colspans of
        // its children
        $colspan = 0;

        foreach ($person['descendants'] as $descendant) {
            $colspan += parsePerson(
                $descendant,
                $rows,
                $level + 1,
                $position + $colspan
            );
        }
    } else {
        // If this person has no children, the colspan is 1.
        $colspan = 1;
    }

    $rows[$level][$position] = [
        'colspan' => $colspan,
        'name'    => $person['name']
    ];

    return $colspan;
}

/**
 * Insert empty cells where needed and sort by keys.
 *
 * @param array $rows
 *
 * @return array
 */
function cleanupRows($rows)
{
    $width = $rows[0][0]['colspan'];
    foreach ($rows as $rowNumber => $row) {
        $spanSoFar = 0;
        foreach ($row as $position => $cell) {
            // Insert empty cells in the row.
            if ($spanSoFar < $position) {
                for ($i = $spanSoFar; $i < $position; $i++) {
                    $rows[$rowNumber][$i] = ['name' => '', 'colspan' => 1];
                    $spanSoFar += 1;
                }
            }
            $spanSoFar += $cell['colspan'];
        }
        // Insert empty cells at the end of the row.
        if ($spanSoFar < $width) {
            for ($i = $spanSoFar; $i < $width; $i++) {
                $rows[$rowNumber][$i] = ['name' => '', 'colspan' => 1];
            }
        }
        // Sort cells by index.
        ksort($rows[$rowNumber]);
    }
    // Sort rows by index.
    ksort($rows);

    return $rows;
}

/**
 * Convert the table array from vertical representation to horizontal
 * representation.
 *
 * @param array $rows
 *
 * @return array
 */
function convertRowsToHorizontal($rows)
{
    // Create a new array containing all fields for the vertical representation
    // of the table.
    $newRows = [];

    // Fill the new array with data from the vertical table.
    foreach ($rows as $rowNumber => $row) {
        foreach ($row as $cellNumber => $cell) {
            $newRows[$cellNumber][$rowNumber] = [
                'name'    => $cell['name'],
                'rowspan' => $cell['colspan']
            ];
        }
    }

    ksort($newRows);

    return $newRows;
}

/**
 * Print the table.
 *
 * @param array $rows
 */
function output($rows)
{
    echo '<table border="1">';
    foreach ($rows as $row) {
        echo '<tr>';
        foreach ($row as $cell) {
            if (!empty($cell['colspan'])) {
                echo '<td colspan="' . $cell['colspan'] . '" align="center">';
            } else {
                echo '<td rowspan="' . $cell['rowspan'] . '" align="center">';
            }
            echo $cell['name'];
            echo '</td>';
        }
        echo '</tr>';
    }
    echo '</table>';
}

As you can see most of the data in your sample array (children, siblings and level) is not used, so you might as well simplify the array structure:

<?php

$tree = [
    'Igor' => [
        'Rapid'   => [
            'Hodor'    => [
                'Hodor II' => [
                    'Hodor III' => null
                ]
            ],
            'Rapid II' => null
        ],
        'Thunder' => [
            'Thunder II' => [
                'Thunder III' => [
                    'Thunder IV' => [
                        'Thunder V' => null
                    ]
                ]
            ]
        ]
    ]
];

// Loop over the tree. Every person in the root of the tree
// gets his own table(s).
foreach ($tree as $name => $children) {
    $table = [];
    parsePerson($name, $children, $table);
    $table = cleanupRows($table);

    output($table);
    $table = convertRowsToHorizontal($table);
    output($table, true);
}

/**
 * Convert a person in the tree to an array to be used to print the tables.
 * The span of a person is either the sum of its children's spans,
 * or 1 if it has no children.
 *
 * @param string $name
 * @param array  $children
 * @param array  $table
 * @param int    $level
 * @param int    $position
 *
 * @return int
 */
function parsePerson($name, $children, &$table, $level = 0, $position = 0)
{
    if (!empty($children)) {
        $span = 0;

        foreach ($children as $childName => $childChildren) {
            $span += parsePerson(
                $childName,
                $childChildren,
                $table,
                $level + 1,
                $position + $span
            );
        }
    } else {
        $span = 1;
    }

    $table[$level][$position] = getCell($name, $span);;

    return $span;
}

/**
 * Insert empty cells where needed and sort by keys.
 *
 * @param array $table
 *
 * @return array
 */
function cleanupRows($table)
{
    $width = $table[0][0]['span'];

    foreach ($table as $rowNumber => $row) {
        $spanSoFar = 0;
        foreach ($row as $position => $cell) {
            addExtraCells($table, $spanSoFar, $rowNumber, $position);
            $spanSoFar += $cell['span'];
        }
        addExtraCells($table, $spanSoFar, $rowNumber, $width);
        ksort($table[$rowNumber]);
    }
    ksort($table);

    return $table;
}

/**
 * @param array $table
 * @param int   $spanSoFar
 * @param int   $rowNumber
 * @param int   $position
 */
function addExtraCells(&$table, &$spanSoFar, $rowNumber, $position)
{
    while ($spanSoFar < $position) {
        $table[$rowNumber][$spanSoFar] = getCell();
        $spanSoFar += 1;
    }
}

/**
 * @param string $name
 * @param int    $span
 *
 * @return array
 */
function getCell($name = '', $span = 1)
{
    return ['name' => $name, 'span' => $span];
}

/**
 * Convert the table array from vertical representation to horizontal
 * representation. By switching 1st and 2nd level array keys.
 *
 * @param array $table
 *
 * @return array
 */
function convertRowsToHorizontal($table)
{
    $horizontal = [];

    foreach ($table as $rowNumber => $row) {
        foreach ($row as $cellNumber => $cell) {
            $horizontal[$cellNumber][$rowNumber] = $cell;
        }
    }
    ksort($horizontal);

    return $horizontal;
}

/**
 * Print the table.
 *
 * @param array $table
 * @param bool  $horizontal
 */
function output($table, $horizontal = false)
{
    $colRow = $horizontal ? 'row' : 'col';

    echo '<table border="1">';
    foreach ($table as $row) {
        echo '<tr>';
        foreach ($row as $cell) {
            echo '<td ' . $colRow . 'span="' . $cell['span'];
            echo '" align="center">';
            echo $cell['name'];
            echo '</td>';
        }
        echo '</tr>';
    }
    echo '</table>';
}

I know this doesn't necessarily fulfill your requirements, but thought I'd throw it out here as an alternative to tables.

The base of the code is a recursive function, much like what the other answers are, I imagine.

function getChildren($tree)
{
    $html = "";
    if (is_array($tree) && count($tree)) {
        $html .= "<ul>\n";
        foreach ($tree as $key=>$leaf) {
            $info = "ID: $key\nChildren: $leaf[children]\nSiblings: $leaf[siblings]\nLevel: $leaf[level]";
            $info = htmlspecialchars($info);
            $name = htmlspecialchars($leaf["name"]);
            $html .= "<li>\n<a href='#' title='$info'>$name</a>\n";
            if (isset($leaf["descendants"])) {
                $html .= getChildren($leaf["descendants"]);
            }
            $html .= "</li>\n";
        }
        $html .= "</ul>\n";
    }
    return $html;
}

$who = array_values($tree)[0]["name"];
$html = getChildren($tree);

What I've done differently is use this CSS-based family tree that can work with very simple markup. It's more aesthetically pleasing and maintains the hierarchical structure of the family tree better (IMO.)

Results:

 * { margin: 0; padding: 0; } .tree ul { padding-top: 20px; position: relative; transition: all 0.5s; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; } .tree li { float: left; text-align: center; list-style-type: none; position: relative; padding: 20px 5px 0 5px; transition: all 0.5s; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; } /*We will use ::before and ::after to draw the connectors*/ .tree li::before, .tree li::after { content: ''; position: absolute; top: 0; right: 50%; border-top: 1px solid #ccc; width: 50%; height: 20px; } .tree li::after { right: auto; left: 50%; border-left: 1px solid #ccc; } /*We need to remove left-right connectors from elements without any siblings*/ .tree li:only-child::after, .tree li:only-child::before { display: none; } /*Remove space from the top of single children*/ .tree li:only-child { padding-top: 0; } /*Remove left connector from first child and right connector from last child*/ .tree li:first-child::before, .tree li:last-child::after { border: 0 none; } /*Adding back the vertical connector to the last nodes*/ .tree li:last-child::before { border-right: 1px solid #ccc; border-radius: 0 5px 0 0; -webkit-border-radius: 0 5px 0 0; -moz-border-radius: 0 5px 0 0; } .tree li:first-child::after { border-radius: 5px 0 0 0; -webkit-border-radius: 5px 0 0 0; -moz-border-radius: 5px 0 0 0; } /*Time to add downward connectors from parents*/ .tree ul ul::before { content: ''; position: absolute; top: 0; left: 50%; border-left: 1px solid #ccc; width: 0; height: 20px; } .tree li a { border: 1px solid #ccc; padding: 5px 10px; text-decoration: none; color: #666; font-family: arial, verdana, tahoma; font-size: 11px; display: inline-block; border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; transition: all 0.5s; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; } /*Time for some hover effects*/ /*We will apply the hover effect the the lineage of the element also*/ .tree li a:hover, .tree li a:hover+ul li a { background: #c8e4f8; color: #000; border: 1px solid #94a0b4; } /*Connector styles on hover*/ .tree li a:hover+ul li::after, .tree li a:hover+ul li::before, .tree li a:hover+ul::before, .tree li a:hover+ul ul::before { border-color: #94a0b4; } 
 <div class="tree"> <ul> <li> <a href='#' title='ID: 1&#13;Children: 2&#13;Siblings: 0&#13;Level: 1'>Igor</a> <ul> <li> <a href='#' title='ID: 7&#13;Children: 2&#13;Siblings: 1&#13;Level: 2'>Rapid</a> <ul> <li> <a href='#' title='ID: 8&#13;Children: 1&#13;Siblings: 1&#13;Level: 3'>Hodor</a> <ul> <li> <a href='#' title='ID: 9&#13;Children: 1&#13;Siblings: 0&#13;Level: 4'>Hodor II</a> <ul> <li> <a href='#' title='ID: 20&#13;Children: 0&#13;Siblings: 0&#13;Level: 5'>Hodor III</a> </li> </ul> </li> </ul> </li> <li> <a href='#' title='ID: 14&#13;Children: 0&#13;Siblings: 1&#13;Level: 3'>Rapid II</a> </li> </ul> </li> <li> <a href='#' title='ID: 2&#13;Children: 0&#13;Siblings: 1&#13;Level: 2'>Thunder</a> </li> </ul> </li> </ul> </div> 

Well u gotta style this, but let's go hacky:

<?php
$tree = [
      [
        'name'        => 'Igor',
        'children'    => 2,
        'siblings'    => 0,
        'level'       => 1,
        'descendants' => [
            [
                'name'        => 'Rapid',
                'children'    => 2,
                'siblings'    => 1,
                'level'       => 2,
                'descendants' => [
                    [
                        'name'        => 'Hodor',
                        'children'    => 1,
                        'siblings'    => 1,
                        'level'       => 3,
                        'descendants' => [
                            [
                                'name'        => 'Hodor II',
                                'children'    => 1,
                                'siblings'    => 0,
                                'level'       => 4,
                                'descendants' => [
                                    [
                                        'name'     => 'Hodor III',
                                        'children' => 0,
                                        'siblings' => 0,
                                        'level'    => 5
                                    ]
                                ]
                            ]
                        ]
                    ],
                    [
                        'name'     => 'Rapid II',
                        'children' => 0,
                        'siblings' => 1,
                        'level'    => 3
                    ]
                ]
            ],
            [
                'name'     => 'Thunder',
                'children' => 0,
                'siblings' => 1,
                'level'    => 2
            ]
        ]
    ]
];

// option 1
echo buildTableV($tree);
echo buildTableH($tree);
// opt 2
echo buildTable($tree, '<td>', ['', ''], '</td>');  // H
echo buildTable($tree, '</tr><tr>', ['<td>', '</td>'] );  // V

function buildTable($t, $a, $b=['', ''] , $c='')
{
    if (!isset($t['name'])) $t = $t[0];

    $o = '<table border="1">';
    $o .=  "<tr><td>" . $t['name'] . "</td>" . $a;
    if (isset($t['descendants'])){
      foreach ($t['descendants'] as $key => $son) {
        $o .=  $b[0] . buildTable($son, $a, $b) . $b[1];
      }
    }
    $o .=  $c . '</tr></table>';

    return $o;
}

function buildTableV($t)
{
    if (!isset($t['name'])) $t = $t[0];

    $o = '<table border="1">';
    $o .=  "<tr><td>" . $t['name'] . "</td></tr><tr>";
    if (isset($t['descendants'])){
      foreach ($t['descendants'] as $key => $son) {
        $o .=  "<td>" . buildTableV($son) . "</td>";
      }
    }
    $o .=  '</tr></table>';

    return $o;
}

function buildTableH($t)
{
    if (!isset($t['name'])) $t = $t[0];

    $o = '<table border="1">';
    $o .=  "<tr><td>" . $t['name'] . "</td><td>";
    if (isset($t['descendants'])){
      foreach ($t['descendants'] as $key => $son) {
        $o .=  "" . buildTableH($son) . "";
      }
    }
    $o .=  '</td></tr></table>';

    return $o;
}

在此处输入图片说明

Using table tr and td code will be very complex as you need to manage colspan and row span, you can use just ul li and then do some css trick will give you desired output.

Please have a look on below solution, I used simple php recursion function to generate html and then added some piece of css, and its done :)

PHP Code

$tree = array(
    array(
        'name'        => 'Igor',
        'children'    => 2,
        'siblings'    => 0,
        'level'       => 1,
        'descendants' => array(
            array(
                'name'        => 'Rapid',
                'children'    => 2,
                'siblings'    => 1,
                'level'       => 2,
                'descendants' => array(
                    array(
                        'name'        => 'Hodor',
                        'children'    => 1,
                        'siblings'    => 1,
                        'level'       => 3,
                        'descendants' => array(
                            array(
                                'name'        => 'Hodor II',
                                'children'    => 1,
                                'siblings'    => 0,
                                'level'       => 4,
                                'descendants' => array(
                                    array(
                                        'name'     => 'Hodor III',
                                        'children' => 0,
                                        'siblings' => 0,
                                        'level'    => 5
                                    )
                                )
                            )
                        )
                    ),
                    array(
                        'name'     => 'Rapid II',
                        'children' => 0,
                        'siblings' => 1,
                        'level'    => 3
                    )
                )
            ),
            array(
                'name'     => 'Thunder',
                'children' => 0,
                'siblings' => 1,
                'level'    => 2
            )
        )
    )
);
echo '<pre>';
function recurseTree($array){

  foreach($array as $v){
      $out .= '<li class="taxon">';
      $out .= '<div class="label">'.$v['name'].'</div>';

    if(is_array($v['descendants'])){
      $out .= '<ul  class="wrapper">'.recurseTree($v['descendants']).'</ul>';
    }
    $out .= '</li>';
  }
  return $out;
}

echo '<div class="horizontal"><ul class="wrapper">'.recurseTree($tree).'</ul>';
echo '<br />';
echo '<br />';
echo '<br />';
echo '<div class="verticle"><ul class="wrapper">'.recurseTree($tree).'</ul>';

CSS Code

<style>
.horizontal .label{
    border-radius: 1px;
    text-align: center;
}

.horizontal .wrapper{
    vertical-align: middle;
}

.horizontal .label, .horizontal .wrapper{
    display: table-cell;
    vertical-align: middle;             
}

.horizontal .taxon{
    display: table-row;
    overflow: hidden;
    outline: 1px solid #ddd;
    text-align: left;
    border-spacing: 5px;
}

.verticle .label{
    border-radius: 1px;
    text-align: center;
}

.verticle .wrapper{
    vertical-align: middle;
}

.verticle .label, .verticle .wrapper{
    display: table-row;
    vertical-align: middle;             
}

.verticle .taxon{
    display: table-cell;
    overflow: hidden;
    outline: 1px solid #ddd;
    text-align: left;
    border-spacing: 5px;
}
</style>

In code wrap whole html with horizontal class it will display it in horizontal format and verticle class will display it in vertical. So same code can be used for both format.

Output Screenshot 在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM