导读:本期聚焦于小伙伴创作的《如何用纯HTML和JavaScript绘制一个自适应且可交互的鱼骨图?》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《如何用纯HTML和JavaScript绘制一个自适应且可交互的鱼骨图?》有用,将其分享出去将是对创作者最好的鼓励。

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

如何用纯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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。