返回博客

Recharts 图表在 flex 卡片里变白块:ResponsiveContainer 的宽度塌陷陷阱

在组件库的状态展示页里,我们把三张 AreaChart 卡片并排放入一个 flex items-center justify-center 的容器。同一个图表组件在"预览"Tab 宽屏展示时渲染正常,切到卡片 Gallery 后三格全是空白方块——没有报错、没有数据异常、SVG 节点存在,就是什么都不画。

第一反应是数据没传下去,或者动画时序问题,折腾了一圈才发现:这是纯粹的 CSS 布局尺寸 bug

现象

  • 同一个 <ResponsiveContainer width="100%" height={280}> 组件,宽容器里正常,flex 卡片里空白。
  • 浏览器控制台出现:The width(0) and height(0) of chart should be greater than 0, please check the style of container, or add an explicit width and height to the chart.
  • jsdom 单元测试里 container.querySelector("svg") 永远返回 null,SVG 从未渲染。

根因

ResponsiveContainer 通过 ResizeObserver 量自己的父元素来决定图表宽高。关键链条是:

  1. 父元素是 flex items-center justify-center(或 inline-flexw-fit)——这类容器是收缩适配(shrink-to-fit),自身宽度由子内容决定。
  2. 图表 wrapper 写了 w-full(即 width: 100%)——百分比宽度向上查找有明确宽度的包含块才能解析,收缩容器没有明确宽度,解析结果是 0
  3. ResponsiveContainer 量到宽度 0,SVG viewport 为 0×280,所有绘制路径都被裁剪掉,画面空白。

症状极具迷惑性:宽容器里 w-full 有效(父有明确宽),卡片里 w-full 失效(父收缩),导致看起来像数据或渲染条件问题,实际只是尺寸计算问题。

坑:w-full 在 flex 收缩父容器里解析为 0

width: 100%(Tailwind w-full)依赖包含块有明确宽度。把它放在 flex items-center justify-center / inline-flex / w-fit 的子项里,宽度解析结果是 0——ResponsiveContainer 量到的父宽也是 0,图表不渲染。

正解

给图表 wrapper 换成显式固定宽度,让 ResponsiveContainer 有真实数值可量,同时用 max-w-full 保持窄屏响应性。

// ❌ 在收缩 flex 容器里,w-full → 宽度 0,图表空白
<div className="w-full max-w-xl" style={{ height: 280 }}>
  <ResponsiveContainer width="100%" height="100%">
    <AreaChart data={data}>…</AreaChart>
  </ResponsiveContainer>
</div>
 
// ✅ 固定宽度给出可量的基准;max-w-full 防止窄屏溢出
<div className="w-[32rem] max-w-full" style={{ height: 280 }}>
  <ResponsiveContainer width="100%" height="100%">
    <AreaChart data={data}>…</AreaChart>
  </ResponsiveContainer>
</div>

如果你能修改父容器:给它加 flex: 1 1 auto + min-width: 0,或者直接停止收缩布局。但改图表 wrapper 更可移植——不影响共享布局里的其他组件。

正解:显式宽度 + max-w-full 双保险

w-[32rem] max-w-full:固定宽给 ResizeObserver 一个真实基准,max-w-full 保证在窄父容器里不溢出。height 本就需要显式设置,这一点 recharts 官方文档也有提示。

jsdom 单元测试变种

jsdom 里 getBoundingClientRect 全返回 0,ResizeObserver 也量不到尺寸,导致 ResponsiveContainer 永远不克隆子组件、SVG 不出现。用 vi.mock 绕过它:

vi.mock("recharts", async (importOriginal) => {
  const actual = await importOriginal<typeof import("recharts")>();
  const { cloneElement } = await import("react");
  return {
    ...actual,
    ResponsiveContainer: ({ children }: { children: any }) =>
      cloneElement(children, { width: 600, height: 300 }),
  };
});

这模拟了 ResponsiveContainer 的真实行为(它本来就是把量到的宽高 clone 进子组件),只是把"量"改成了写死数字。之后 container.querySelector("svg") 就能拿到节点了。

注意:视觉正确性(图形是否真的画出来、dark/light token 对不对)还是要靠截图验证,DOM 断言只能确认 SVG 存在,无法确认里面有没有路径被裁掉。

一句话外卖

ResponsiveContainer 量父元素——把它扔进 flex 收缩容器时,w-full 解析为 0,用 w-[显式值] max-w-full 代替。