鱼骨图作为分析问题的常用工具,在网页端展示时往往需要适配不同屏幕尺寸,同时支持用户交互操作。下面我们通过纯HTML和JavaScript实现一个满足需求的鱼骨图。

实现思路概述
整个实现分为三个核心部分:首先是数据结构定义,用来存储鱼骨图的节点信息;其次是自适应布局计算,根据容器尺寸和节点数量调整每个节点的位置;最后是交互逻辑绑定,实现节点的点击反馈、展开收起等功能。
数据结构定义
我们用一个嵌套的对象结构来表示鱼骨图的节点,每个节点包含自身内容、子节点列表、是否展开等属性:
// 鱼骨图节点数据结构
const fishboneData = {
id: 'root',
content: '核心问题:页面加载缓慢',
children: [
{
id: 'cause1',
content: '网络因素',
children: [
{ id: 'cause1-1', content: '带宽不足', children: [] },
{ id: 'cause1-2', content: 'DNS解析慢', children: [] }
]
},
{
id: 'cause2',
content: '代码因素',
children: [
{ id: 'cause2-1', content: '冗余代码过多', children: [] },
{ id: 'cause2-2', content: '未做代码压缩', children: [] }
]
}
]
};
自适应布局实现
自适应布局的核心是监听容器尺寸变化,重新计算所有节点的坐标。我们使用ResizeObserver来监听容器尺寸,根据容器宽度和节点层级计算横向和纵向位置。
布局计算逻辑
鱼骨图的主骨水平放置,分支骨从主骨两侧交替延伸。计算规则如下:
- 主骨水平居中放置在容器中间,长度根据容器宽度自适应
- 一级分支骨从主骨上下交替排列,间距根据容器高度和分支数量计算
- 子节点沿分支骨垂直排列,间距固定为40px
下面是布局计算的核心代码:
// 获取容器元素
const container = document.getElementById('fishbone-container');
// 存储计算后的节点位置信息
const nodePositions = {};
// 布局计算函数
function calculateLayout() {
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
// 主骨参数
const mainBoneXStart = 50;
const mainBoneXEnd = containerWidth - 50;
const mainBoneY = containerHeight / 2;
// 记录根节点位置
nodePositions['root'] = { x: mainBoneXStart, y: mainBoneY };
// 计算一级分支
const firstLevelCauses = fishboneData.children;
const level1Gap = (containerHeight - 100) / (firstLevelCauses.length + 1);
firstLevelCauses.forEach((cause, index) => {
// 上下交替排列
const isTop = index % 2 === 0;
const causeY = isTop ? mainBoneY - (index + 1) * level1Gap : mainBoneY + (index + 1) * level1Gap;
// 分支骨连接主骨的位置
const connectX = mainBoneXStart + (mainBoneXEnd - mainBoneXStart) * (index + 1) / (firstLevelCauses.length + 1);
nodePositions[cause.id] = { x: connectX, y: causeY, connectX, connectY: mainBoneY };
// 计算子节点位置
calculateChildPositions(cause, connectX, causeY, isTop);
});
}
// 递归计算子节点位置
function calculateChildPositions(parentNode, parentX, parentY, isTop) {
const children = parentNode.children;
if (children.length === 0) return;
const childGap = 40;
children.forEach((child, index) => {
const childX = parentX + 60;
const childY = isTop ? parentY - (children.length * childGap) / 2 + index * childGap : parentY - (children.length * childGap) / 2 + index * childGap;
nodePositions[child.id] = { x: childX, y: childY, connectX: parentX, connectY: parentY };
// 递归处理更深层级子节点
calculateChildPositions(child, childX, childY, isTop);
});
}
绘制鱼骨图
我们使用HTML的canvas元素来绘制鱼骨图的线条和节点,这样既能保证绘制性能,也方便做交互判断。
// 初始化canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
container.appendChild(canvas);
// 调整canvas尺寸匹配容器
function resizeCanvas() {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
calculateLayout();
drawFishbone();
}
// 绘制鱼骨图
function drawFishbone() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制主骨
ctx.beginPath();
ctx.moveTo(nodePositions['root'].x, nodePositions['root'].y);
ctx.lineTo(nodePositions['root'].x + (canvas.width - 100), nodePositions['root'].y);
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.stroke();
// 递归绘制分支和节点
function drawNode(node) {
const pos = nodePositions[node.id];
if (!pos) return;
// 绘制连接线(如果是子节点)
if (pos.connectX !== undefined) {
ctx.beginPath();
ctx.moveTo(pos.connectX, pos.connectY);
ctx.lineTo(pos.x, pos.y);
ctx.strokeStyle = '#666';
ctx.lineWidth = 2;
ctx.stroke();
}
// 绘制节点文本
ctx.fillStyle = node.id === 'root' ? '#ff4444' : '#333';
ctx.font = node.id === 'root' ? '16px Arial' : '14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(node.content, pos.x, pos.y);
// 递归绘制子节点
node.children.forEach(child => drawNode(child));
}
drawNode(fishboneData);
}
// 监听容器尺寸变化
const resizeObserver = new ResizeObserver(() => resizeCanvas());
resizeObserver.observe(container);
// 初始绘制
resizeCanvas();
添加交互功能
常见的交互需求包括点击节点高亮、点击分支节点展开收起子节点,我们通过监听canvas的点击事件实现这些功能。
// 存储当前高亮节点ID
let activeNodeId = null;
// 判断点击位置是否在节点文本范围内
function getClickedNode(x, y) {
for (const id in nodePositions) {
const pos = nodePositions[id];
// 简单判断点击范围,实际可根据文本宽度调整
const textWidth = ctx.measureText(fishboneData.content).width;
if (x >= pos.x - textWidth/2 && x <= pos.x + textWidth/2 && y >= pos.y - 10 && y <= pos.y + 10) {
return id;
}
}
return null;
}
// 点击事件监听
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const clickedId = getClickedNode(clickX, clickY);
if (clickedId) {
activeNodeId = clickedId;
// 重新绘制,高亮点击的节点
drawFishbone();
// 高亮逻辑:给点击的节点加背景
const pos = nodePositions[clickedId];
ctx.fillStyle = 'rgba(0, 150, 255, 0.2)';
ctx.fillRect(pos.x - 50, pos.y - 15, 100, 30);
ctx.fillStyle = '#333';
ctx.fillText(fishboneData.content, pos.x, pos.y);
}
});
总结
通过上述步骤,我们就实现了一个纯HTML和JavaScript的自适应可交互鱼骨图。整个实现没有依赖任何第三方库,布局会根据容器尺寸自动调整,同时支持节点点击高亮等基础交互。如果需要在ipipp.com等域名下部署,只需要将相关资源地址替换即可正常运行。开发者可以根据实际需求扩展更多交互功能,比如节点编辑、导出图片等。
HTMLJavaScript鱼骨图自适应布局交互功能修改时间:2026-07-04 21:45:33