简单渲染
计算机文件目录这类树都是从上到下渲染树的。这个比较简单,掌握一定数据结构知识便可(主要是队列 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")