nivo — React + d3 组件化图表
是什么
nivo 是把 d3 的图表能力包成一组 React 组件的库。日常类比:d3 像一堆零件(量尺、画笔、布局算法),nivo 把这堆零件装成成品玩具——你只要说”我要一个柱状图,数据是这些”,玩具自己出来了。
你写:
import { ResponsiveBar } from '@nivo/bar'
<ResponsiveBar data={[{ country: 'CN', value: 30 }, { country: 'US', value: 45 }]} keys={['value']} indexBy='country'/>这一行就能渲一张可交互的柱状图。没碰过 d3.select、没写过 enter/exit。这种”props 进,svg 出”的写法,就是 nivo 的核心承诺。
为什么重要
不理解 nivo 解决的问题,下面这些事都不好理解:
- 为什么 d3 那么强大但在 React 项目里不好用——两个 DOM 引擎打架
- 为什么 React 仪表板首屏要么白屏闪烁、要么直接出图——SSR 友好与否的差别
- 为什么 Storybook 是组件库的”活文档”——每个 prop 都能实时调
- 为什么数据多了图就卡——SVG 节点上限 vs Canvas 像素绘制
核心要点
nivo 的设计可以拆成 三个分工:
-
d3 只算,不画。d3-scale 算坐标映射,d3-shape 生成 path 字符串,d3-hierarchy 算树/树形图布局。这些函数返回的是数据,不是 DOM。
-
React 只画,不算。算好的坐标和路径作为 props 传给 React 组件,React 渲成
<svg><rect /><path /></svg>。整个过程没有d3.select(...).enter().append()。 -
react-spring 动画,不用 d3-transition。组件卸载时动画自然停,不会留”幽灵节点”。
合起来,d3 与 React 各司其职,互不打架。
实践案例
案例 1:一张柱状图的完整 props
<ResponsiveBar data={data} keys={['hot dog', 'burger']} indexBy='country' margin={{ top: 50, right: 130, bottom: 50, left: 60 }} colors={{ scheme: 'nivo' }} theme={{ axis: { ticks: { text: { fontSize: 12 } } } }}/>逐字段解释:
data:数据数组,每行是一个柱子组keys:每组里有几根柱子(堆叠或并排)indexBy:x 轴用哪个字段当类目margin / colors / theme:外观——通过 React Context 下传给所有子图元
案例 2:SVG vs Canvas 同一份 props
import { ResponsiveBar } from '@nivo/bar' // SVG,DOM 可点import { ResponsiveBarCanvas } from '@nivo/bar' // Canvas,性能版数据点几百以内用 SVG,每根柱子是 <rect>,能加 hover/click。几千个点以上换 Canvas——React 只调 ctx.fillRect,没有 DOM 节点。代价是不能用 CSS 选到具体柱子,要走 nivo 提供的 hover 事件。
案例 3:SSR 渲完直接发
// Next.js / Astro 服务端import { Bar } from '@nivo/bar' // 注意:固定尺寸版本,不是 Responsive
<Bar width={800} height={400} data={data} keys={['v']} indexBy='c' />服务端 React 渲完是一段 <svg>...</svg> 字符串,直接发给浏览器,首屏不闪。Responsive 版本在服务端会找不到 ResizeObserver——所以 SSR 必须用固定尺寸版本,到客户端再换 Responsive。
踩过的坑
-
import 整个 nivo 包体积爆炸——用
@nivo/bar而不是顶层nivo。每个图表是独立 npm 包,按需引入才能 tree-shake。 -
Responsive 父容器塌成 0——
ResponsiveBar用 ResizeObserver 测父容器,如果父级是height: auto,测到 0 就什么都不画。要么显式给父级高度,要么用固定尺寸版本。 -
Canvas 版 e2e 测试取不到元素——没有 DOM 节点意味着
getByRole('img')抓到的是整张 canvas。要测某个柱子,得走 nivo 的onMouseMove事件拿到数据。 -
TypeScript 自定义 tooltip 类型严——nivo 的 generics 推不动你自定义的 datum,要手写
BarTooltipProps<MyDatum>。 -
theme 不会自动跟随 dark mode——主题对象是静态的,要自己监听
prefers-color-scheme切换 theme prop。
适用 vs 不适用场景
适用:
- React 仪表板、Next.js / Astro / Remix 项目里需要图表
- 数据点几百到几千,需要交互(hover、点击下钻)
- 需要 Storybook 风格的文档/playground
- 需要服务端渲染首屏图
不适用:
- 不是 React 项目——直接用 d3 或 Chart.js
- 数据点上万——用 Canvas 也吃力,考虑 deck.gl / regl 走 WebGL
- 需要完全自定义图表形态(不是标准柱/线/饼/散点)——用 visx 或裸 d3 自己拼
- 静态展示用 PNG 就够——可以用 nivo 的 HTTP API 服务端生成图片
历史与生态
- 2017:作者 Raphael Benitte 发布第一版,目标”把 d3 React 化”
- 2018–2020:陆续加 Canvas 变体、HTTP API(服务端渲 PNG/JSON)、整体 TypeScript 重写
- 2022 起:迁到 pnpm monorepo,30 多个
@nivo/*包独立发版 - 2026 现状:约 13k star,社区图表库三巨头之一(与 Recharts、visx 并列)
与同类对比
- vs Recharts:Recharts 把图表拆成
<XAxis /><YAxis /><Bar />子组件像乐高拼;nivo 一个组件一张图,配置都走 props 对象。Recharts 适合”我要细调每个轴”,nivo 适合”我要一张能用的图”。 - vs visx(Airbnb):visx 是 d3 的 React 原语包,给你
<Scale><Shape>自己拼;nivo 是封装好的高层组件。visx 灵活但要懂 d3,nivo 开箱即用但定制有边界。 - vs 纯 d3:纯 d3 命令式(select / enter / exit),灵活但与 React 心智模型冲突;nivo 牺牲一部分灵活换 React 习惯。
- vs Chart.js:Chart.js 是 Canvas-only vanilla JS;nivo 是 React 双轨(SVG + Canvas)。
学到什么
- 声明式包装命令式——d3 命令式 API(selection、enter、exit)跟 React 声明式 vDOM 冲突,nivo 的解法是只用 d3 算坐标,渲染交还 React
- monorepo + 按图表分包——避免”装一个用全部”的体积问题,是中大型组件库标配
- Storybook 即文档——每个 prop 一个 story,比 README 直观
- SSR 友好的代价——Responsive 在 server 不可用,要按”固定尺寸 server 端 + Responsive 客户端”分层
- react-spring 替代 d3-transition——动画系统也要跟着框架走,否则卸载留残骸
延伸阅读
- 官方文档(Storybook 形态):nivo.rocks
- 源码入口:github.com/plouc/nivo
- 作者 Raphael Benitte 的 d3+React 思路文章(GitHub 仓库 README 链接)
- react-server-components —— 现代 SSR 的演化方向,解释 nivo 为何要在服务端渲完直发
- playwright —— Canvas 版 e2e 测试的兜底工具(hover 事件抽数据)
关联
- react-server-components —— 服务端渲染的现代方案,与 nivo SSR 友好同根
- starlight —— 同样是 Astro 生态,文档站点常用 nivo 嵌图
- playwright —— Canvas 版图表的 e2e 测试方案
- tanstack-router —— 同属”用 React 心智重写传统库”的代表