如何绘制一棵树

简单渲染

计算机文件目录这类树都是从上到下渲染树的。这个比较简单,掌握一定数据结构知识便可(主要是队列 queue、栈 stack)。

如下图我写了个简单的函数渲染树。

html 复制代码
<html>
<head>
    <title>test</title>
    <style>
        /* Reset and base styles */
        * {
            box-sizing: border-box;
        }

        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }

        /* Tree container */
        .tree {
            max-width: 600px;
            margin: 0 auto;
        }

        /* Main tree list - remove default styling */
        .tree>ul {
            list-style: none;
            padding-left: 0;
            margin: 0;
        }

        /* All nested lists */
        .tree ul {
            list-style: none;
            padding-left: 0;
            margin: 0;
        }

        /* List items - vertical stacking */
        .tree li {
            position: relative;
            margin-bottom: 8px;
        }

        /* Box styling */
        .box {
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 12px 16px;
            background-color: #f8f9fa;
            transition: all 0.2s ease;
            cursor: pointer;
        }

        .box:hover {
            background-color: #e9ecef;
            border-color: #007bff;
            box-shadow: 0 2px 4px rgba(0, 123, 255, 0.1);
        }

        /* Links styling */
        .box a {
            text-decoration: none;
            color: #333;
            font-weight: 500;
            display: block;
        }

        .box a:hover {
            color: #007bff;
        }

        /* Tree structure visual indicators */
        .tree ul ul {
            margin-left: 24px;
            position: relative;
        }

        .tree ul ul:before {
            content: '';
            position: absolute;
            left: -16px;
            top: 0;
            bottom: 0;
            width: 1px;
            background-color: #dee2e6;
        }

        .tree ul ul li:before {
            content: '';
            position: absolute;
            left: -16px;
            top: 16px;
            width: 12px;
            height: 1px;
            background-color: #dee2e6;
        }

        /* Level-specific styling for better hierarchy */
        .tree>ul>li>.box {
            background-color: #007bff;
            color: white;
            font-weight: bold;
        }

        .tree>ul>li>.box a {
            color: white;
        }

        .tree>ul>li>.box:hover {
            background-color: #0056b3;
        }

        /* Second level */
        .tree ul ul li .box {
            background-color: #28a745;
            color: white;
        }

        .tree ul ul li .box a {
            color: white;
        }

        .tree ul ul li .box:hover {
            background-color: #1e7e34;
        }

        /* Third level and deeper */
        .tree ul ul ul li .box {
            background-color: #6f42c1;
            color: white;
        }

        .tree ul ul ul li .box a {
            color: white;
        }

        .tree ul ul ul li .box:hover {
            background-color: #5a359a;
        }

        /* Responsive design */
        @media (max-width: 768px) {
            .tree {
                margin: 0 10px;
            }

            .tree ul ul {
                margin-left: 16px;
            }
        }
    </style>
</head>

<body>
    <div class="nav">

    </div>

    <br />
    <br />

    <div class="tree"></div>
    <script>
        const tree = [
            {
                path: 'home',
                name: 'Home',
            },
            {
                path: 'about',
                name: 'About US',
            },
            {
                path: 'product',
                name: 'Product Center',
                children: [
                    {
                        path: 'phone',
                        name: 'Phone',
                    },
                    {
                        path: 'pc',
                        name: 'PC',
                        children: [
                            {
                                path: 'pc',
                                name: 'PC 1',
                            },
                            {
                                path: 'laptop',
                                name: 'Laptop'
                            }
                        ]
                    },

                ]
            },
            {
                path: 'contact',
                name: 'Contact Us',
                children: [
                    {
                        path: 'message',
                        name: 'Message',
                    },
                    {
                        path: 'map',
                        name: 'Map'
                    }
                ]
            }
        ];

        /*         function search(queue, arr, fullPath, result) {
                    if (queue.length === 0)
                        return;
                    const c = queue.shift();
        
                    for (let i = 0; i < arr.length; i++) {
                        const item = arr[i];
        
                        if (item.path === c) {
                            fullPath = `${fullPath}/${item.path}`;
                            result.push(`<a href="${fullPath}">${item.name}</a> `);
        
                            if (queue.length && item.children) {
                                search(queue, item.children, fullPath, result);
                            }
        
                            break;
                        }
        
                    }
                }
        
                function generateNav(path) {
                    if (path.startsWith('/'))
                        path = path.slice(1);
        
                    if (path.endsWith('/'))
                        path = path.slice(0, -1);
        
                    let result = [];
                    const queue = path.split('/');
                    search(queue, tree, '', result);
        
                    const nav = document.querySelector('.nav');
                    nav.innerHTML = result.join('》');
                }
        
                generateNav('product/pc/laptop'); */

        let html = "";

        // function walkTree(arr, stack) {
        //     html += "<ul>";
        //     for (let i = 0; i < arr.length; i++) {
        //         const item = arr[i];
        //         stack.push(item.path);
        //         const path = stack.join('/');
        //         html += `<li class="level-${stack.length - 1}"><div class="box">
        //             <a href="${path}">${item.name}
        //                 </a></div>`;

        //         if (item.children) {
        //             walkTree(item.children, stack);
        //         }

        //         html += "</li>";
        //         stack.pop();
        //     }

        //     html += "</ul>";
        // }

        function walkTree(arr, stack) {
            const level = stack.length;

            if (level === 0)
                html += "<tr>";
            else
                html += "<ul>";

            for (let i = 0; i < arr.length; i++) {
                const item = arr[i];
                stack.push(item.path);
                const path = stack.join('/');

                html += `<${level === 0 ? 'td' : 'li'} class="level-${level}"><div class="box">
                    <a href="${path}">${item.name}
                        </a></div>`;

                if (item.children) {
                    walkTree(item.children, stack);
                }

                html += `</${level === 0 ? 'td' : 'li'}`;
                stack.pop();
            }


            if (level === 0)
                html += "</tr>";
            else
                html += "</ul>";
        }


        walkTree(tree, []);

        html = html.replace(/^<li>|<\/li>$/gm, "");
        console.log(html)
        document.querySelector('.tree').innerHTML = '<table>' + html + "</table>";
    </script>
</body>
</html>

从左到右是非常简单的,------如果旋转90度,变成从上到下呢?难度不是难了一丁半点......

Ahnentafel

逛维基百科的时候发现下面的一张图,是把一棵树工整地渲染在网页上。究其源码,居然是用 HTML<table>元素一点一点渲染的,------这肯定不是手写 HTML 而是代码生成表格的,------但问题是如何生成复杂的树状结构呢?

幸好逛维基百科多数东西都是开放的,包括这个表格的渲染原理,此为 Ahnentafel-compact5英文简介

{{Ahnentafel}}是一個家譜模板。 對於人來說,圖表通常涵蓋5代。 雖然支持任意數量的級別,但在特殊情況下需要時,建議將圖表擴展到最多6個級別(63個節點),不再需要將內容放入流行的屏幕分辨率。

該模板以圖形種族譜系圖祖先樹的形式呈現家譜數據(實現為HTML table)。 ahnentafel中的條目作為模板的編號參數給出,並且可以包含任意wikimarkup

源码于,却是我不太熟悉的 LUA 所写的。通过 AI 转换一下为 JS。

html 复制代码
<html>
<head>
    <title>Horizontal Tree Navigation</title>
    <style>
        /* 现有的基础样式保持不变 */

        /* 将主树列表的布局设置为水平 */
        .tree>ul {
            display: flex;
            flex-direction: row;
            list-style: none;
            padding-left: 0;
            margin: 0;
        }

        /* 所有嵌套列表现在也是水平的 */
        .tree ul ul {
            display: flex;
            flex-direction: row;
            margin-left: 24px;
            position: relative;
        }

        /* 列表项现在是水平排列 */
        .tree li {
            position: relative;
            margin-right: 8px;
            /* 改为右边距 */
            white-space: nowrap;
            /* 防止文本换行 */
        }


        li {
            list-style: none;
        }
    </style>
</head>

<body>
    <h1>Horizontal Tree Navigation</h1>

    <div class="tree"></div>

    <script>
        /**
         * 生成 Ahnentafel 族谱 HTML 表格 (JS 版)
         * 
         * @param {Object} args - 参数对象,键为数字表示祖先编号,其他为样式控制
         * @returns {string} HTML 字符串
         */
        function ahnentafelChart(args = {}) {
            // 模拟 MediaWiki 的 yesno 功能
            function yesno(val) {
                if (typeof val === 'boolean') 
                    return val;
                if (typeof val !== 'string') 
                    return false;
                const lower = val.trim().toLowerCase();

                return ['yes', 'y', 'true', '1', 'on'].includes(lower);
            }

            // 存储生成的分类(此处忽略,仅保留占位)
            let tcats = ''; // 原 Lua 中用于添加维基分类

            // 参数检查(模拟 checkparameters)
            function checkParameters(k) {
                const allowed = [
                    'align', 'collapsed', 'collapsible', 'title', 'float', 'clear', 'ref',
                    'headnotes', 'headnotes_align', 'footnotes', 'footnotes_align',
                    'width', 'min-width', 'text-align', 'boxstyle', 'style', 'border'
                ];

                if (allowed.includes(k)) 
                    return;

                if (/^boxstyle_[1-8]$/.test(k)) 
                    return;

                if (/^border_[1-8]$/.test(k)) 
                    return;

                // 标记未知参数(可记录日志或忽略)
                // tcats += `<!-- Unknown param: ${k} -->`;
            }

            // 添加单元格(核心渲染函数)
            function addCell(rows, r, rspan, cspan, content, style) {
                if (r + rspan - 1 < rowBegin || r > rowEnd) 
                    return;

                if (r < rowBegin) {
                    rspan -= (rowBegin - r);
                    r = rowBegin;
                }

                if (r + rspan - 1 > rowEnd)
                    rspan = rowEnd + 1 - r;

                if (rspan <= 0)
                    return;

                // 确保行存在
                while (rows.length <= r) rows.push([]);
                const row = rows[r];
                const cell = {
                    content: content || ' ',
                    rowspan: rspan > 1 ? rspan : undefined,
                    colspan: cspan > 1 ? cspan : undefined,
                    style: style || null
                };

                row.push(cell);
            }

            // 主逻辑开始
            let {
                align = '',
                style = '',
                collapsed = '',
                collapsible = '',
                title = '',
                float: floatAlign = '',
                clear = '',
                ref = '',
                headnotes = '',
                headnotes_align = '',
                footnotes = '',
                footnotes_align = '',
                width = '',
                'min-width': minWidth = '',
                'text-align': textAlign = 'center',
                ...ancestors
            } = args;

            // 处理 collapsible 和 collapsed
            if (collapsed !== '' && collapsible === '')
                collapsible = 'yes';
            if (title && collapsible === '')
                collapsible = 'yes';
            if (title && collapsed === '')
                collapsed = yesno(collapsed || 'no') ? 'yes' : 'no';

            // 对齐样式
            align = align.toLowerCase();

            if (align === 'right')
                style = 'float:right;' + style;
            else if (align === 'left')
                style = 'float:left;' + style;
            else if (align === 'center')
                style = 'margin-left:auto; margin-right:auto;' + style;

            // 检查所有参数,找出最大编号
            let maxNum = 0;
            Object.keys(ancestors).forEach(k => {
                if (k === parseInt(k, 10).toString()) {
                    const num = parseInt(k, 10);

                    if (num > maxNum) 
                        maxNum = num;
                } else 
                    checkParameters(k);
            });

            maxNum = Math.min(maxNum, 511); // 限制最大为 511 (9代)
            const levels = Math.ceil(Math.log2(maxNum + 1));
            const totalCells = (1 << levels) - 1; // 2^levels - 1

            // 填充中间缺失的祖先(保证树结构完整)
            for (let k = totalCells; k >= 2; k--) {
                const j = Math.floor(k / 2);
                if (ancestors[k] && ancestors[k] !== '' && !ancestors[j]) {
                    ancestors[j] = ' '; // 占位符
                }
            }

            // 计算表格行范围
            let rowBegin = 2 * totalCells + 1;
            let rowEnd = 2 * totalCells + 2;

            let cellNum = 0;
            for (let l = 1; l <= levels; l++) {
                const cellsInLevel = 1 << (l - 1); // 2^(l-1)
                let offset = 1;
                for (let k = 1; k <= cellsInLevel; k++) {
                    cellNum++;
                    const span = (1 << (levels - l + 1)) - 1;
                    offset += 2 * span;
                    if (ancestors[cellNum]) {
                        rowBegin = Math.min(offset, rowBegin);
                        rowEnd = Math.max(offset + 1, rowEnd);
                    }
                    if (ancestors[cellNum] === '') delete ancestors[cellNum];
                    offset += 2 * span + 4;
                }
            }

            // 开始构建 HTML
            let html = '';
            let innerHtml = '';
            let containerStyle = '';
            let innerFontSize = '88%';
            const topBranchStyle = 'border-top: #000 solid 1px; border-left: #000 solid 1px;';
            const botBranchStyle = 'border-bottom: #000 solid 1px; border-left: #000 solid 1px;';

            // 可折叠容器
            if (yesno(collapsible)) {
                containerStyle += 'border: 1px solid #aaa; ';
                containerStyle += 'min-width: ' + (minWidth || width || '60em') + '; ';
                containerStyle += 'width: ' + (width || 'auto') + '; ';
                containerStyle += 'font-size: 88%; ';
                containerStyle += 'margin: 0.3em auto; ';
                containerStyle += 'clear: ' + (clear || 'none') + '; ';

                if (floatAlign === 'left') {
                    containerStyle = containerStyle.replace('margin: 0.3em auto;', 'margin: 0.3em 1em 0.3em 0; float: left; ')
                        .replace('clear: none;', `clear: ${clear || 'left'};`);
                } else if (floatAlign === 'right') {
                    containerStyle = containerStyle.replace('margin: 0.3em auto;', 'margin: 0.3em 0 0.3em 1em; float: right; ')
                        .replace('clear: none;', `clear: ${clear || 'right'};`);
                } else if (floatAlign === 'none') {
                    containerStyle = containerStyle.replace('margin: 0.3em auto;', 'margin: 0.3em 0;');
                }

                const tableTitle = title || '的先祖';
                innerHtml += `<tr><th style="padding:0.2em 0.3em 0.2em 4.3em;background:none;width:${width || 'auto'}">${tableTitle}${ref || ''}</th></tr>`;
                innerFontSize = null;
                ref = '';
            }

            // 表头注释
            if (headnotes) {
                const alignStyle = headnotes_align ? `text-align:${headnotes_align};` : '';
                innerHtml += `<tr><td style="width:100%;${alignStyle}">${headnotes}</td></tr>`;
            }

            // 构建主表格
            const tableRows = [];
            for (let i = rowBegin; i <= rowEnd + 1; i++) 
                tableRows[i] = [];
            
            // 初始化空行
            for (let i = rowBegin; i <= rowEnd + 1; i++) 
                addCell(tableRows, i, 1, 1, ' ', null);
            
            // 添加顶部空白单元格用于对齐
            const blankCells = 3 * levels + 1;

            if (tableRows[rowEnd + 1]) {
                for (let i = 1; i < blankCells; i++) 
                    addCell(tableRows, rowEnd + 1, 1, 1, ' ', null);
            }

            // 填充每一层
            cellNum = 0;

            for (let l = 1; l <= levels; l++) {
                const levelStyle = (args[`boxstyle_${l}`] || '') +
                    (args['boxstyle'] ? `;${args['boxstyle']}` : '') +
                    `;height:0.5em; padding:0 0.2em;` +
                    `border:${args[`border_${l}`] || args['border'] || '1'}px solid var(--color-emphasized, black);`;

                const cellsInLevel = 1 << (l - 1);
                let offset = 1;

                for (let k = 1; k <= cellsInLevel; k++) {
                    cellNum++;
                    const span = (1 << (levels - l + 1)) - 1;

                    // 上方填充
                    addCell(tableRows, offset, 2 * span, (l < levels) ? 2 : 4, ' ', null);

                    // 上分支线
                    if (l < levels) {
                        addCell(tableRows, offset, span, 1, ' ', null);
                        addCell(tableRows, offset + span, span, 1, ' ', ancestors[2 * cellNum] ? topBranchStyle : null);
                    }

                    offset += 2 * span;

                    // 中间单元格
                    addCell(tableRows, offset, 2, 4, ancestors[cellNum] || ' ', ancestors[cellNum] ? levelStyle : null);
                    if (l < levels) 
                        addCell(tableRows, offset, 2, 3 + 4 * (levels - l - 1), ' ', null);
                    
                    offset += 2;

                    // 下方填充
                    addCell(tableRows, offset, 2 * span, (l < levels) ? 2 : 4, ' ', null);

                    // 下分支线
                    if (l < levels) {
                        addCell(tableRows, offset, span, 1, ' ', ancestors[2 * cellNum + 1] ? botBranchStyle : null);
                        addCell(tableRows, offset + span, span, 1, ' ', null);
                    }

                    offset += 2 * span + 2;
                }
            }

            // 渲染表格行
            let tableHtml = '<table style="border-collapse:separate;border-spacing:0;line-height:130%;' +
                (innerFontSize ? 'font-size:88%;' : '') + style + '">';

            for (let i = rowBegin; i <= rowEnd + 1; i++) {
                if (!tableRows[i]) continue;
                tableHtml += '<tr style="text-align:center">';
                tableRows[i].forEach(cell => {
                    const rowspan = cell.rowspan ? ` rowspan="${cell.rowspan}"` : '';
                    const colspan = cell.colspan ? ` colspan="${cell.colspan}"` : '';
                    const styleAttr = cell.style ? ` style="${cell.style}"` : '';
                    tableHtml += `<td${rowspan}${colspan}${styleAttr}>${cell.content}</td>`;
                });
                tableHtml += '</tr>';
            }
            tableHtml += '</table>';

            innerHtml += `<tr><td style="text-align:${textAlign}">${tableHtml}</td></tr>`;

            // 表尾注释
            if (footnotes || ref) {
                const alignStyle = footnotes_align ? `text-align:${footnotes_align};` : '';
                innerHtml += `<tr><td style="width:100%;${alignStyle}">${ref || ''}${footnotes || ''}</td></tr>`;
            }

            if (yesno(collapsible)) {
                const classes = ['collapsible', yesno(collapsed) ? 'collapsed' : ''].filter(Boolean).join(' ');
                html += `<table class="${classes}" style="${containerStyle}">${innerHtml}</table>`;
            } else 
                html += innerHtml;

            // 返回最终 HTML(包裹防止样式干扰)
            return `<div class="noresize">${html}</div>${tcats}`;
        }

        // --- 使用示例 ---

        const args = {
            1: "本人",
            2: "父亲",
            3: "母亲",
            4: "祖父",
            5: "祖母",
            6: "外祖父",
            7: "外祖母",
            8: "xx外祖母",
            9: "xy外祖母",
            title: "家族族谱",
            collapsible: "yes",
            levels: 3
        };

        document.body.innerHTML = ahnentafelChart(args);
    </script>
</body>
</html>

Ahnentafel 树问题有两个:一、只能是二叉树,即子节点最多有两个,这肯定不能满足多节点树的要求;二、输入的数据结构为一维数组,不够直观且复杂了。

表格方向

如上例,表格渲染的方法是从左到右的,能否改为从上到下呢?继续问 AI,给出了答案:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>垂直带连接线的 Ahnentafel 族谱图</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f7fa;
        }

        #familyTreeContainer {
            max-width: 1000px;
            margin: 0 auto;
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        .collapsible-header {
            cursor: pointer;
            padding: 15px;
            background-color: #e1e8f0;
            border: 1px solid #c9d1d9;
            user-select: none;
            font-weight: bold;
            border-radius: 5px 5px 0 0;
        }

        .collapsible-header::after {
            content: " \25BC";
            /* 向下的三角形 */
            float: right;
        }

        .collapsible-header.collapsed::after {
            content: " \25BA";
            /* 向右的三角形 */
        }

        .collapsible-content {
            overflow: hidden;
            transition: max-height 0.3s ease-out, padding 0.3s ease;
            border: 1px solid #c9d1d9;
            border-top: none;
            border-radius: 0 0 5px 5px;
        }

        .collapsible-content.collapsed {
            max-height: 0 !important;
            padding-top: 0 !important;
            padding-bottom: 0 !important;
        }

        .notes {
            margin: 15px 0;
            padding: 10px 15px;
            background-color: #f0f5ff;
            border-left: 4px solid #7aa5d8;
        }

        .notes-head {
            text-align: left;
        }

        .notes-foot {
            text-align: right;
        }

        /* --- 族谱图核心样式 --- */
        .ahnentafel-tree-container {
            position: relative;
            width: 100%;
            overflow-x: auto;
            /* 如果内容过宽,允许横向滚动 */
            padding: 20px 0;
            background-color: #fafbfc;
        }

        .ahnentafel-tree {
            position: relative;
            height: 600px;
            /* 初始高度,会被JS动态调整 */
            min-width: 800px;
            /* 最小宽度,防止内容过窄 */
        }

        .tree-person {
            position: absolute;
            padding: 10px 15px;
            border: 1px solid #444;
            background-color: #e6f7ff;
            text-align: center;
            white-space: nowrap;
            box-sizing: border-box;
            transform: translate(-50%, -50%);
            /* 以中心点定位 */
            border-radius: 4px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
            z-index: 9999;
        }

        .tree-person.missing {
            background-color: #f6f8fa;
            color: #666;
            border-style: dashed;
            border-color: #999;
        }

        .tree-person.gen-1 {
            background-color: #e6f7ff;
            font-weight: bold;
            border-color: #1890ff;
        }

        .tree-person.gen-2 {
            background-color: #f6ffed;
            border-color: #52c41a;
        }

        .tree-person.gen-3 {
            background-color: #fffbe6;
            border-color: #faad14;
        }

        .tree-person.gen-4 {
            background-color: #fff0f6;
            border-color: #eb2f96;
        }

        .tree-line {
            position: absolute;
            background-color: #333;
        }

        .tree-line.horz {
            height: 1px;
        }

        .tree-line.vert {
            width: 1px;
        }

        .clear {
            clear: both;
        }
    </style>
</head>

<body>

    <div id="familyTreeContainer">
        <div class="collapsible-header" onclick="toggleCollapse(this)">📜 家族族谱 (Ahnentafel)</div>
        <div class="collapsible-content">
            <div class="notes notes-head">📌 注:本图表基于 Ahnentafel 编号系统。数字代表在家族树中的唯一位置。</div>
            <div class="ahnentafel-tree-container">
                <div class="ahnentafel-tree" id="familyTree">
                    <!-- 族谱图将在这里生成 -->
                </div>
            </div>
            <div class="notes notes-foot">📄 数据来源:示例数据</div>
            <div class="clear"></div>
        </div>
    </div>

    <script>
        /**
         * 生成从上到下带连接线的 Ahnentafel 族谱图 (使用绝对定位)
         * @param {Object} containerElement - 包含族谱图的DOM容器元素
         * @param {Object} args - 包含祖先数据和配置的参数对象
         */
        function ahnentafelVerticalChartWithAbsolutePosition(containerElement, args = {}) {
            // --- 参数处理 ---
            const {
                // 样式和配置参数
                box_width = 120,
                box_height = 50,
                level_y_gap = 100, // 代与代之间的垂直间距
                line_color = '#333',
                ...ancestors
            } = args;

            // 清空容器
            containerElement.innerHTML = '';

            // 计算最大编号,确定代数
            let maxNum = 0;
            for (const k in ancestors) {
                if (ancestors.hasOwnProperty(k)) {
                    const num = parseInt(k, 10);
                    if (!isNaN(num) && num > 0) {
                        maxNum = Math.max(maxNum, num);
                    }
                }
            }

            if (maxNum === 0) {
                containerElement.innerHTML = '<div style="text-align:center; padding:20px;">暂无族谱数据</div>';
                return;
            }

            maxNum = Math.min(maxNum, 511); // 最多支持 9 代
            const levels = Math.ceil(Math.log2(maxNum + 1)); // 代数

            // 存储所有人物节点的DOM元素和坐标,方便后续绘制连接线
            const nodes = {};

            // --- 创建人物框 ---
            for (let gen = 1; gen <= levels; gen++) {
                const start = 2 ** (gen - 1);  // 该代起始编号
                const end = Math.min(2 ** gen - 1, maxNum); // 该代结束编号

                for (let num = start; num <= end; num++) {
                    const name = ancestors[num] !== undefined ? ancestors[num] : '';
                    const isMissing = !name;
                    const display = name ? `[${num}] ${name}` : `[${num}] ?`;

                    const personDiv = document.createElement('div');
                    personDiv.className = `tree-person gen-${gen} ${isMissing ? 'missing' : ''}`;
                    personDiv.textContent = display;
                    personDiv.style.width = `${box_width}px`;
                    personDiv.style.height = `${box_height}px`;

                    // 计算位置
                    // 在当前代中是第几个 (从0开始)
                    const indexInGen = num - start;
                    // 当前代总人数
                    const totalInGen = end - start + 1;
                    // 计算中心X坐标 (相对于容器宽度的百分比)
                    const centerXPercent = ((indexInGen + 0.5) / totalInGen) * 100;
                    // Y坐标
                    const centerY = (gen - 1) * level_y_gap + box_height / 2;

                    personDiv.style.left = `${centerXPercent}%`;
                    personDiv.style.top = `${centerY + 20}px`;

                    containerElement.appendChild(personDiv);

                    // 存储节点信息
                    nodes[num] = {
                        element: personDiv,
                        centerXPercent: centerXPercent,
                        centerY: centerY
                    };
                }
            }

            // --- 绘制连接线 ---
            for (let num = 1; num <= maxNum; num++) {
                if (!nodes[num]) continue; // 跳过不存在的节点

                const parentNode = nodes[num];
                const childNum1 = num * 2;
                const childNum2 = num * 2 + 1;

                if (nodes[childNum1] || nodes[childNum2]) {
                    // 1. 从父节点底部画一条垂直线
                    const vertLine = document.createElement('div');
                    vertLine.className = 'tree-line vert';
                    const lineTop = parentNode.centerY + box_height / 2;
                    const lineHeight = level_y_gap - box_height;
                    vertLine.style.left = `${parentNode.centerXPercent}%`;
                    vertLine.style.top = `${lineTop}px`;
                    vertLine.style.height = `${lineHeight}px`;
                    containerElement.appendChild(vertLine);

                    // 2. 如果有子节点,画水平线和子节点的垂直线
                    if (nodes[childNum1] || nodes[childNum2]) {
                        const childrenCenterY = lineTop + lineHeight;

                        // 找到子节点的X坐标范围
                        let leftXPercent = parentNode.centerXPercent;
                        let rightXPercent = parentNode.centerXPercent;
                        if (nodes[childNum1]) {
                            leftXPercent = nodes[childNum1].centerXPercent;
                        }
                        if (nodes[childNum2]) {
                            rightXPercent = nodes[childNum2].centerXPercent;
                        }

                        // 画水平线
                        if (leftXPercent !== rightXPercent) {
                            const horzLine = document.createElement('div');
                            horzLine.className = 'tree-line horz';
                            horzLine.style.top = `${childrenCenterY}px`;
                            horzLine.style.left = `${leftXPercent}%`;
                            horzLine.style.width = `${rightXPercent - leftXPercent}%`;
                            containerElement.appendChild(horzLine);
                        }

                        // 为每个存在的子节点画垂直短线
                        [childNum1, childNum2].forEach(childNum => {
                            if (nodes[childNum]) {
                                const childVertLine = document.createElement('div');
                                childVertLine.className = 'tree-line vert';
                                childVertLine.style.left = `${nodes[childNum].centerXPercent}%`;
                                childVertLine.style.top = `${childrenCenterY}px`;
                                childVertLine.style.height = `${box_height / 2}px`;
                                containerElement.appendChild(childVertLine);
                            }
                        });
                    }
                }
            }

            // --- 动态调整容器高度 ---
            const totalHeight = (levels - 1) * level_y_gap + box_height + 50; // 50 为底部留白
            containerElement.style.height = `${totalHeight}px`;
        }

        // --- 使用示例 ---
        const sampleData = {
            1: "本人",
            2: "父亲",
            3: "母亲",
            4: "祖父",
            5: "祖母",
            6: "外祖父",
            7: "外祖母",
            8: "曾祖父",
            9: "曾祖母",
            10: "曾外祖父",
            11: "曾外祖母",
            12: "高祖父",
            13: "高祖母",
            14: "高外祖父",
            15: "高外祖母"
        };

        // 渲染到页面
        const treeContainer = document.getElementById('familyTree');
        ahnentafelVerticalChartWithAbsolutePosition(treeContainer, sampleData);

        // --- 折叠/展开功能 ---
        function toggleCollapse(headerElement) {
            const content = headerElement.nextElementSibling;
            headerElement.classList.toggle('collapsed');
            content.classList.toggle('collapsed');

            // 动态计算并设置 max-height 以实现平滑过渡
            if (!content.classList.contains('collapsed')) {
                content.style.maxHeight = content.scrollHeight + "px";
                content.style.paddingTop = "10px";
                content.style.paddingBottom = "10px";
            } else {
                content.style.maxHeight = content.scrollHeight + "px"; // 先设为实际高度
                // 触发重排
                content.offsetHeight;
                content.style.maxHeight = "0";
                content.style.paddingTop = "0";
                content.style.paddingBottom = "0";
            }
        }

        // 初始化折叠状态
        window.addEventListener('load', function () {
            const content = document.querySelector('.collapsible-content');
            if (content) {
                content.style.maxHeight = content.scrollHeight + "px";
                content.style.paddingTop = "10px";
                content.style.paddingBottom = "10px";
            }
        });
        // 确保在内容可能变化后(如窗口大小改变导致scrollHeight变化)也能正确折叠
        window.addEventListener('resize', function () {
            const content = document.querySelector('.collapsible-content');
            if (content && !content.classList.contains('collapsed')) {
                content.style.maxHeight = content.scrollHeight + "px";
            }
        });
    </script>
</body>
</html>

渲染复杂树结构

对于复杂的树结构,还需要算法:Reingold-Tilford。这是一个例子:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reingold-Tilford 算法 - 多叉树 (修正连线)</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f7fa;
        }

        #treeContainer {
            max-width: 1200px;
            margin: 0 auto;
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        .header {
            text-align: center;
            margin-bottom: 20px;
        }

        .tree-canvas-container {
            position: relative;
            width: 100%;
            overflow: auto;
            background-color: #fafbfc;
            border: 1px solid #e1e4e8;
            border-radius: 4px;
        }

        .tree-canvas {
            position: relative;
            min-width: 100%;
            min-height: 500px; /* 初始最小高度 */
        }

        .tree-node {
            position: absolute;
            padding: 8px 12px;
            background-color: #e1efff;
            border: 1px solid #a3c6ff;
            border-radius: 4px;
            text-align: center;
            white-space: nowrap;
            box-sizing: border-box;
            transform: translate(-50%, -50%);
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            font-size: 14px;
            cursor: default;
        }

        .tree-node.root { background-color: #ffe7e1; border-color: #ffafa3; font-weight: bold; }
        .tree-node:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.2); z-index: 10; }

        .tree-link {
            position: absolute;
            background-color: #555;
            transform-origin: 0 0;
        }
        .tree-link.horz { height: 1px; }
        .tree-link.vert { width: 1px; }

        .instructions {
            margin-top: 20px;
            padding: 10px;
            background-color: #f0f5ff;
            border-left: 4px solid #7aa5d8;
            font-size: 14px;
        }
    </style>
</head>
<body>

<div id="treeContainer">
    <div class="header">
        <h2>🌳 Reingold-Tilford 算法 - 多叉树可视化 (修正连线)</h2>
    </div>
    <div class="tree-canvas-container">
        <div class="tree-canvas" id="treeCanvas">
            <!-- 树将被绘制在这里 -->
        </div>
    </div>
    <div class="instructions">
        <p><strong>说明:</strong> 此图使用 Reingold-Tilford 算法进行布局。每个节点可以有任意数量的子节点。算法确保节点不重叠,布局紧凑且对称。<br>
        <strong>连线修正:</strong> 现在每条连接线都包含完整的三部分:父节点底部的垂直线、层级间的水平线、子节点顶部的垂直线。</p>
    </div>
</div>

<script>
    /**
     * 树节点类
     */
    class TreeNode {
        constructor(id, name = 'Node') {
            this.id = id;
            this.name = name;
            this.children = [];
            this.parent = null;

            // RT 算法使用的属性
            this.prelim = 0;      // 初步的x坐标 (相对)
            this.mod = 0;         // 子树需要整体移动的距离
            this.shift = 0;       // 兄弟节点之间需要移动的距离
            this.change = 0;      // 用于向祖先传递移动信息
            this.tl = null;       // 左兄弟 (Thread/Left Neighbor)
            this.tr = null;       // 右兄弟 (Thread/Right Neighbor)
            this.el = null;       // 左轮廓 (Extreme Left)
            this.er = null;       // 右轮廓 (Extreme Right)
            this.msel = 0;        // 左子树间的最小sep
            this.mser = 0;        // 右子树间的最小sep
        }

        isLeaf() {
            return this.children.length === 0;
        }

        hasLeft() {
            return this.tl !== null;
        }

        // 获取前一个兄弟节点
        getLeftSibling() {
            if (this.parent && this.parent.children.length > 0) {
                const index = this.parent.children.indexOf(this);
                if (index > 0) {
                    return this.parent.children[index - 1];
                }
            }
            return null;
        }
    }

    /**
     * Reingold-Tilford 树布局类
     */
    class ReingoldTilfordTree {
        constructor(rootNode, options = {}) {
            this.root = rootNode;
            this.options = {
                nodeWidth: 80,
                nodeHeight: 30,
                levelHeight: 70,
                siblingsMargin: 20,
                subtreeMargin: 20,
                ...options
            };
            this.nodeMap = new Map(); // 用于快速查找节点
            this._indexNodes(this.root);
        }

        // 为所有节点建立索引
        _indexNodes(node) {
            this.nodeMap.set(node.id, node);
            for (const child of node.children) {
                this._indexNodes(child);
            }
        }

        // 主要的布局函数
        layout() {
            if (!this.root) return;

            this._firstWalk(this.root);
            this._secondWalk(this.root, 0);
            this._calculateCanvasSize();
        }

        // 第一次遍历:计算初步x坐标和mod
        _firstWalk(v) {
            if (v.isLeaf()) {
                const leftSibling = v.getLeftSibling();
                if (leftSibling) {
                    v.prelim = leftSibling.prelim + this.options.siblingsMargin + this.options.nodeWidth;
                } else {
                    v.prelim = 0;
                }
            } else {
                let defaultAncestor = v.children[0]; // 默认祖先节点
                for (let i = 0; i < v.children.length; i++) {
                    const w = v.children[i];
                    this._firstWalk(w);
                    defaultAncestor = this._apportion(w, defaultAncestor);
                }
                this._executeShifts(v);
                
                // 将父节点放在子节点的水平中点
                const firstChild = v.children[0];
                const lastChild = v.children[v.children.length - 1];
                const midpoint = (firstChild.prelim + lastChild.prelim) / 2;

                const leftSibling = v.getLeftSibling();
                if (leftSibling) {
                    v.prelim = leftSibling.prelim + this.options.siblingsMargin + this.options.nodeWidth;
                    v.mod = v.prelim - midpoint;
                } else {
                    v.prelim = midpoint;
                }
            }
        }

        // 调整子树位置,解决冲突
        _apportion(v, defaultAncestor) {
            const leftSibling = v.getLeftSibling();
            if (leftSibling) {
                let vip = v; // 内侧轮廓
                let vop = v; // 外侧轮廓
                let vim = leftSibling; // 内侧轮廓的兄弟
                let vom = vip.parent.children[0]; // 外侧轮廓的兄弟

                let sip = vip.mod;
                let sop = vop.mod;
                let sim = vim.mod;
                let som = vom.mod;

                while (this._nextRight(vim) && this._nextLeft(vip)) {
                    vim = this._nextRight(vim);
                    vip = this._nextLeft(vip);
                    vom = this._nextLeft(vom);
                    vop = this._nextRight(vop);

                    vop.tr = v; // 设置线索

                    const shift = (vim.prelim + sim) - (vip.prelim + sip) + this.options.subtreeMargin + this.options.nodeWidth;
                    
                    if (shift > 0) {
                        const ancestor = this._ancestor(vim, v);
                        this._moveSubtree(ancestor === defaultAncestor ? v : defaultAncestor, v, shift);
                        sip += shift;
                        sop += shift;
                    }
                    
                    sim += vim.mod;
                    sip += vip.mod;
                    som += vom.mod;
                    sop += vop.mod;
                }

                if (this._nextRight(vim) && !this._nextRight(vop)) {
                    vop.tr = this._nextRight(vim);
                    vop.mod += sim - sop;
                }
                if (this._nextLeft(vip) && !this._nextLeft(vom)) {
                    vom.tr = this._nextLeft(vip);
                    vom.mod += sip - som;
                    defaultAncestor = v;
                }
            }
            return defaultAncestor;
        }

        // 辅助函数:移动子树
        _moveSubtree(wl, wr, shift) {
            const subtrees = wr.parent.children.indexOf(wr) - wr.parent.children.indexOf(wl);
            if (subtrees > 0) {
                wr.change -= shift / subtrees;
                wr.shift += shift;
                wl.change += shift / subtrees;
                wr.prelim += shift;
                wr.mod += shift;
            }
        }

        // 辅助函数:执行shift
        _executeShifts(v) {
            let shift = 0;
            let change = 0;
            for (let i = v.children.length - 1; i >= 0; i--) {
                const w = v.children[i];
                w.prelim += shift;
                w.mod += shift;
                change += w.change;
                shift += w.shift + change;
            }
        }

        // 辅助函数:找祖先
        _ancestor(vim, v) {
            const parent = v.parent;
            if (parent && parent.children.includes(vim.tr)) {
                return vim.tr;
            }
            return vim.tl; // 默认返回左兄弟
        }

        // 辅助函数:找下一个右轮廓节点
        _nextRight(v) {
            if (v.children.length > 0) {
                return v.children[v.children.length - 1];
            } else {
                return v.tr;
            }
        }

        // 辅助函数:找下一个左轮廓节点
        _nextLeft(v) {
            if (v.children.length > 0) {
                return v.children[0];
            } else {
                return v.tl;
            }
        }

        // 第二次遍历:计算最终绝对坐标
        _secondWalk(v, modsum) {
            v.x = v.prelim + modsum;
            v.y = v.level * this.options.levelHeight;

            for (const child of v.children) {
                child.level = (v.level || 0) + 1;
                this._secondWalk(child, modsum + v.mod);
            }
        }

        // 计算画布大小
        _calculateCanvasSize() {
            let minX = Infinity, maxX = -Infinity, maxY = -Infinity;
            const traverse = (node) => {
                if (node.x < minX) minX = node.x;
                if (node.x > maxX) maxX = node.x;
                if (node.y > maxY) maxY = node.y;
                for (const child of node.children) {
                    traverse(child);
                }
            };
            traverse(this.root);

            this.minX = minX;
            this.maxX = maxX;
            this.maxY = maxY;
            this.width = (maxX - minX) + this.options.nodeWidth * 2;
            this.height = maxY + this.options.levelHeight;
        }

        // 渲染到HTML容器
        render(containerElement) {
            containerElement.innerHTML = '';
            containerElement.style.width = `${this.width}px`;
            containerElement.style.height = `${this.height}px`;
            containerElement.style.minWidth = '100%';
            containerElement.style.minHeight = '500px';

            const offsetX = -this.minX + this.options.nodeWidth; // 保证最左节点不被裁剪
            const offsetY = this.options.levelHeight / 2; // 顶部留白

            // 递归渲染节点和连线
            const renderNode = (node) => {
                const nodeDiv = document.createElement('div');
                nodeDiv.className = `tree-node ${node === this.root ? 'root' : ''}`;
                nodeDiv.textContent = node.name;
                nodeDiv.title = `ID: ${node.id}`;
                nodeDiv.style.left = `${node.x + offsetX}px`;
                nodeDiv.style.top = `${node.y + offsetY}px`;
                nodeDiv.style.width = `${this.options.nodeWidth}px`;
                nodeDiv.style.height = `${this.options.nodeHeight}px`;
                containerElement.appendChild(nodeDiv);

                for (const child of node.children) {
                    // 绘制连线
                    this._drawLink(containerElement, node, child, offsetX, offsetY);
                    renderNode(child);
                }
            };

            if (this.root) {
                this.root.level = 0; // 设置根节点层级
                renderNode(this.root);
            }
        }

        // 绘制节点间的连接线 (修正版)
        _drawLink(container, parent, child, offsetX, offsetY) {
            // 父节点底部中心
            const startX = parent.x + offsetX;
            const startY = parent.y + offsetY + this.options.nodeHeight / 2;
            
            // 子节点顶部中心
            const endX = child.x + offsetX;
            const endY = child.y + offsetY - this.options.nodeHeight / 2;

            // 水平线的 Y 坐标 (在父节点和子节点之间)
            const horzY = (startY + endY) / 2;

            // 1. 从父节点底部画垂直线到水平线
            const vertLine1 = document.createElement('div');
            vertLine1.className = 'tree-link vert';
            vertLine1.style.left = `${startX}px`;
            vertLine1.style.top = `${startY}px`;
            vertLine1.style.height = `${horzY - startY}px`;
            container.appendChild(vertLine1);

            // 2. 画水平线
            const horzLine = document.createElement('div');
            horzLine.className = 'tree-link horz';
            horzLine.style.left = `${Math.min(startX, endX)}px`;
            horzLine.style.top = `${horzY}px`;
            horzLine.style.width = `${Math.abs(endX - startX)}px`;
            container.appendChild(horzLine);

            // 3. 从水平线画垂直线到子节点顶部
            const vertLine2 = document.createElement('div');
            vertLine2.className = 'tree-link vert';
            vertLine2.style.left = `${endX}px`;
            vertLine2.style.top = `${horzY}px`;
            vertLine2.style.height = `${endY - horzY}px`;
            container.appendChild(vertLine2);
        }
    }

    // --- 示例数据和使用 ---
    function createSampleTree() {
        // 创建节点
        const nodes = {};
        const nodeNames = [
            "1-CEO", "2-VP Sales", "3-VP Engineering", "4-VP Marketing",
            "5-Sales Manager A", "6-Sales Manager B", "7-Lead Engineer", "8-Architect",
            "9-Marketing Lead", "10-Intern",
            "11-Sales Rep 1", "12-Sales Rep 2", "13-Sales Rep 3",
            "14-Junior Dev", "15-Designer", "16-Senior Dev",
            "17-Marketing Specialist"
        ];

        nodeNames.forEach((name, index) => {
            const id = (index + 1).toString();
            nodes[id] = new TreeNode(id, name);
        });

        // 构建树关系
        nodes["1"].children = [nodes["2"], nodes["3"], nodes["4"]];
        nodes["2"].parent = nodes["1"]; nodes["3"].parent = nodes["1"]; nodes["4"].parent = nodes["1"];

        nodes["2"].children = [nodes["5"], nodes["6"]];
        nodes["5"].parent = nodes["2"]; nodes["6"].parent = nodes["2"];

        nodes["3"].children = [nodes["7"], nodes["8"]];
        nodes["7"].parent = nodes["3"]; nodes["8"].parent = nodes["3"];

        nodes["4"].children = [nodes["9"], nodes["10"]];
        nodes["9"].parent = nodes["4"]; nodes["10"].parent = nodes["4"];

        nodes["5"].children = [nodes["11"], nodes["12"]];
        nodes["11"].parent = nodes["5"]; nodes["12"].parent = nodes["5"];

        nodes["6"].children = [nodes["13"]];
        nodes["13"].parent = nodes["6"];

        nodes["7"].children = [nodes["14"], nodes["15"]];
        nodes["14"].parent = nodes["7"]; nodes["15"].parent = nodes["7"];

        nodes["8"].children = [nodes["16"]];
        nodes["16"].parent = nodes["8"];

        nodes["9"].children = [nodes["17"]];
        nodes["17"].parent = nodes["9"];

        return nodes["1"]; // 返回根节点
    }

    // --- 初始化和渲染 ---
    window.addEventListener('load', () => {
        const sampleRoot = createSampleTree();
        const rtTree = new ReingoldTilfordTree(sampleRoot, {
            nodeWidth: 100,
            nodeHeight: 35,
            levelHeight: 80,
            siblingsMargin: 25,
            subtreeMargin: 25
        });

        rtTree.layout();

        const canvas = document.getElementById('treeCanvas');
        rtTree.render(canvas);
    });

</script>

</body>
</html>

参考:

  • 基于JavaScript和d3使用RT算法实现的radial tree layout:blog.csdn.net/m0_51653200...
  • 绘制可展现的树 www.cnblogs.com/zhongzihao/...
  • 翻译\] 树结构自动布局算法 [zhuanlan.zhihu.com/p/573659320](https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F573659320 "https://zhuanlan.zhihu.com/p/573659320")

相关推荐
carver w2 小时前
c++,数据结构,unordermap哈希表基本操作
数据结构·c++·散列表
带鱼吃猫7 小时前
高并发内存池(三):手把手从零搭建ThreadCache线程缓存
数据结构·c++·链表·visual studio
yongui478347 小时前
INTLAB区间工具箱在区间分析算法中的应用与实现
数据结构·算法
wewe_daisy8 小时前
python、数据结构
开发语言·数据结构·python
洲覆14 小时前
C++ constexpr 修饰符与函数
开发语言·数据结构·c++
Tiny番茄17 小时前
146. LRU缓存
数据结构·leetcode·缓存
spiderwiner18 小时前
Part03 数据结构
数据结构·c++·csp
小欣加油20 小时前
leetcode 206 反转链表
数据结构·c++·算法·leetcode·链表·职场和发展
野犬寒鸦20 小时前
力扣hot100:环形链表II(哈希算法与快慢指针法思路讲解)
java·数据结构·算法·leetcode·链表·哈希算法