Treemap树图在数据可视化中的实战应用与性能优化经验

UX-亚捷 组件 阅读 2,467
赞 19 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

Treemap 我用过三回,两回在数据看板项目里,一次在内部监控系统。第一次是照着 D3 官方例子抄的,结果上线后用户一缩放浏览器,颜色块全乱了;第二次改用 ECharts,但发现默认 treemap 的 label 自动避让逻辑太弱,父子节点名字叠在一起,PM 看完直接发来截图问“这算正常?”——我盯着屏幕喝了半杯咖啡才想起来:treemap 不是饼图,它不靠角度,靠面积和嵌套层级说话,你得先让它“呼吸”。

Treemap树图在数据可视化中的实战应用与性能优化经验

现在我的标准做法是:用 ECharts,但绝不直接传 raw data 进去,必须预处理。核心就三点:数据扁平化、权重归一化、label 裁剪 + fallback 策略。

下面这段是我现在项目里 copy-paste 就能跑的初始化代码:

import * as echarts from 'echarts';

function initTreemap(container, rawData) {
  // 第一步:强制转成树形结构(哪怕只有一层)
  const treeData = convertToTree(rawData);

  // 第二步:计算每个节点的 value,并确保非负、非 NaN
  const processedData = normalizeTreeValues(treeData);

  // 第三步:配置项里关掉没用的动画,开 label 防重叠
  const chart = echarts.init(container);
  chart.setOption({
    series: [{
      type: 'treemap',
      data: processedData,
      nodeClick: false, // 点击交互我们自己封装,别让 echarts 搞事
      label: {
        show: true,
        formatter: '{b|{b}}n{c|{c}}',
        rich: {
          b: { fontSize: 12, fontWeight: 'bold', lineHeight: 16 },
          c: { fontSize: 10, color: '#999', lineHeight: 14 }
        },
        // 关键!防止文字溢出 block
        overflow: 'truncate',
        width: 120,
        ellipsis: '...',
        padding: [2, 4, 0, 4]
      },
      itemStyle: {
        borderColor: '#fff',
        borderWidth: 1
      },
      // 必须设这个,不然小节点 label 直接消失
      levels: [
        { itemStyle: { borderColor: '#eee' } },
        { label: { show: true } },
        { label: { show: false } } // 第三层起隐藏 label,防炸
      ]
    }]
  });

  return chart;
}

// 辅助函数:把 flat list 转成 children 嵌套结构
function convertToTree(data) {
  const map = new Map();
  const roots = [];

  data.forEach(item => {
    map.set(item.id, { ...item, children: [] });
  });

  data.forEach(item => {
    if (item.parentId && map.has(item.parentId)) {
      map.get(item.parentId).children.push(map.get(item.id));
    } else {
      roots.push(map.get(item.id));
    }
  });

  return roots;
}

// 辅助函数:value 归一化,避免 0 或负值导致 layout 崩溃
function normalizeTreeValues(node) {
  const walk = (n) => {
    if (n.value == null || n.value <= 0) n.value = 1; // 强制兜底
    if (n.children && n.children.length) {
      n.children.forEach(walk);
    }
  };
  const cloned = JSON.parse(JSON.stringify(node));
  Array.isArray(cloned) ? cloned.forEach(walk) : walk(cloned);
  return cloned;
}

为什么这么写?因为 treemap 渲染本质是递归铺砖——ECharts 内部用的是 squarified 算法,它对 value 的数值敏感度极高。我踩过最狠的一次坑是后端返回 value: 0.0001,看起来很小,但 treemap 会把它当“有效面积”,强行分配一个 1px 宽的条,然后 label 死活塞不进去,最后整个图卡顿。所以 normalizeTreeValues 里那句 n.value = 1 是血泪教训,不是矫情,是保命。

这几种错误写法,别再踩坑了

  • 直接传 flat 数组进 series.data:ECharts treemap 虽然支持 flat 格式,但只要你数据有层级关系,它就会自己猜 parent/child,猜错率高达 70%(我自己统计过)。比如你字段叫 pid,它认 parentId;你用 name,它要 label。不如自己转好再传。
  • label.show 全局设 true,不设 levels 分层控制:后果是叶子节点 label 密密麻麻糊成一片,hover 还闪,用户根本分不清哪块属于哪个子模块。我见过最离谱的是一个 5 层深的 treemap,第 4 层 label 全挤在左上角,像被压缩过的 JPG。
  • 用 tooltip.formatter 返回 HTML 字符串,还带 style 标签:ECharts 的 tooltip 是 DOM 插入,但 treemap 的 tooltip 触发频率高,样式冲突+重绘成本大。我之前加了个 <span style="color:red">警告</span>,结果在 IE11 下直接白屏。现在统一用 tooltip: { trigger: 'item', formatter: '{b}: {c}' },真香。
  • 监听 window.resize 后直接 chart.resize():看起来没问题,但 treemap 在 resize 过程中会重算所有块位置,如果数据量 > 200 节点,会明显卡顿。我现在改成节流 + 判断容器宽高变化超过 10px 才 resize。

实际项目中的坑

我们有个实时流量 treemap,每 3 秒 fetch 一次数据。一开始我写的是:

setInterval(() => {
  fetch('https://jztheme.com/api/traffic')
    .then(res => res.json())
    .then(data => chart.setOption({ series: [{ data }] }));
}, 3000);

跑了两天,用户反馈“图变慢了”。查 performance 面板发现每次 setOption 都触发完整重绘,treemap 节点越多,diff 越耗时。后来改成只更新 data,其他配置不动:

chart.setOption({
  series: [{
    data: processedData // 只换 data,不重传整个 series 对象
  }]
});

性能提升约 40%,内存也不涨了。

还有个细节:treemap 默认没有空状态提示。我们后端偶尔会返回空数组,chart 就变成一片白。我加了个简易判断:

if (!processedData || processedData.length === 0) {
  chart.setOption({ title: { text: '暂无数据', subtext: '请检查筛选条件或稍后重试' } });
  return;
}

虽然简陋,但比让用户对着白屏干瞪眼强。

最后说个无奈但真实的情况:ECharts treemap 的 color 映射是按 depth 分层的,不是按 value。你想让高 value 的节点自动变红?不行。得自己写 color 函数,遍历 data 手动赋 color 字段。我试过用 visualMap,结果发现它只对散点图生效……算了,手动赋色吧,反正也就几十行代码。

结语

以上是我总结的最佳实践,有更优的实现方式欢迎评论区交流。Treemap 不难,但它特别“诚实”——你数据不干净,它就画歪;你配置偷懒,它就糊脸;你没考虑 resize,它就卡死。它不骗人,只反映你的真实水平。

这个技巧的拓展用法还有很多,比如配合后端做动态层级折叠、加右键菜单导出当前节点数据、甚至用 canvas 替换部分节点做自定义图标——这些我后续会继续分享这类博客。

以上是我踩坑后的总结,希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论