billboard.js — c3.js 的 TypeScript 继任者
是什么
billboard.js 是 Naver 出品的 D3 v7 封装图表库,定位是”接住停滞的 c3.js”。日常类比:c3.js 像一台 2014 年的相机,能用但厂家不再出固件;billboard.js 是同一家做相机的另一个团队拿出新机身、按键布局完全照搬,老用户连说明书都不用换。
你写:
bb.generate({ bindto: '#chart', data: { columns: [['销量', 30, 200, 100, 400, 150]], type: 'line' }})API 几乎和 c3.js 一字不差——这就是它的核心承诺:老 c3 项目改个 import 就跑。
为什么重要
不理解 billboard.js 的定位,下面这些事都没法解释:
- 为什么韩国互联网公司(Naver / Kakao 等)后台看板大量用它,而不是 ECharts
- 为什么 2018 年之后 c3.js 项目在 GitHub 评论区都被人推荐”换 billboard”
- 为什么它有 TypeScript 类型却仍走”配置对象 + bindto” 的命令式 API——继承 c3 的历史包袱
- 为什么它支持的图类型比 c3.js 多一倍(sunburst / treemap / sankey / radar)
核心要点
billboard.js 的设计可以拆成 三个承诺:
-
API 兼容 c3.js:
generate({ bindto, data, axis, legend })字段几乎对齐。类比:换发动机不换方向盘——司机还是同一个司机。 -
TypeScript 全量重写:每个 option 都有类型,IDE 补全友好。这是 c3 永远做不到的(c3 是纯 JS)。类比:从手写菜单升级成有图有价的扫码菜单。
-
D3 v7 + 模块化加载:底层换上现代 D3,按图类型分包,按需 import 才能 tree-shake 出小 bundle。类比:自助餐拆成档口,要哪盘点哪盘,不用全端上来。
三件加起来叫 平滑继承——老用户零成本迁移,新用户拿到的是 TS 友好的现代库。
实践案例
案例 1:从 c3 迁移过来的最小改动
// 老代码(c3.js)import c3 from 'c3'c3.generate({ bindto: '#chart', data: { columns: [['A', 1, 2, 3]] } })
// 新代码(billboard.js)import bb from 'billboard.js'bb.generate({ bindto: '#chart', data: { columns: [['A', 1, 2, 3]] } })只改了 import 名。这就是 billboard 卖点最直接的体现——存量项目升级路径几乎为零成本。
案例 2:按需加载省 bundle
import bb, { line, bar } from 'billboard.js'bb.generate({ bindto: '#chart', data: { columns: [['A', 1, 2, 3, 4, 5]], type: line() }, // 只 import 用到的图类型,未用的 pie/gauge/sankey 不会进 bundle})type: line() 而不是 type: 'line' 是新写法——它把图类型当成可摇树的模块。生产项目压完几十 KB 就够。
案例 3:TypeScript 提示一眼能看清配置
import bb, { ChartOptions } from 'billboard.js'const options: ChartOptions = { bindto: '#chart', data: { columns: [['A', 30, 200, 100]], type: 'line' }, axis: { y: { tick: { format: (v) => `${v} 件` } } }}bb.generate(options)ChartOptions 类型让你写到 axis.y.tick 时 IDE 直接列出所有字段——c3.js 时代靠记文档,现在交给编译器。
踩过的坑
-
不是 React 组件,是命令式 API:
bindto接 DOM 选择器,所以在 React 里要在useEffect里手动 generate 并保存实例 destroy。新人常把 ref.current 直接传进去然后 React 18 严格模式双调用导致重复实例。 -
API “几乎”兼容 c3 不是”100%” 兼容:
onrendered回调签名 /data.onclick参数顺序 / 主题文件路径都微调过,盲目复制 c3 老代码会有沉默 bug。 -
D3 v7 ESM 化导致 IE11 死路:billboard 3.x 起依赖现代 ESM 浏览器,老国内政府项目还要 IE11 的话只能停留在 2.x 或换其它库。
-
多主题切换需要换 CSS 文件不是切 option:
billboard.css/billboard-datalab.css/billboard-graph.css是四份独立 CSS,要在 build 阶段决定,不能运行时切——这一点和 ECharts 不一样。 -
bundle 没按需 import 时很大:
import bb from 'billboard.js'会把所有图类型打进来,几百 KB。生产必须显式import { line }这种 named export 才能 tree-shake。 -
resize 必须手动触发:容器宽度被父级 flex 改变时图不会自动重画,要调
chart.resize()或自行监听 ResizeObserver。c3 时代踩过的坑这里没修。 -
TypeScript 类型有时和实际行为不一致:极少数 option(特别是新加的 sankey / treemap)类型还在补,IDE 报红但运行时其实接受——遇到时翻 GitHub issue 比读类型定义快。
适用 vs 不适用场景
适用:
- 老 c3.js 项目升级,想要 TS 类型 + 维护活跃
- 企业后台 dashboard:折线 / 柱 / 饼 / radar / treemap 等常规图齐全
- 团队已经懂 D3 v7,需要”省心默认 + 必要时下钻到 D3”
- 韩国 / 日本 / 国内 toB 后台项目(生态熟、issue 响应快)
不适用:
- 极度自定义视觉(每根柱独立形变 / 自定义动画曲线) → 直接用 D3
- 移动端追求最小 bundle → Chart.js 更轻
- 复杂关系图 / 地理 / 3D → ECharts 或 deck.gl
- 需要服务器端渲染(SSR)首屏出图 → billboard 强依赖 DOM,要走 jsdom workaround
历史小故事(可跳过)
- 2014 年:Masayuki Tanaka 一个人写了 c3.js,把 D3 包成”配置式 API”,迅速火起来。
- 2017 年:c3 长期由作者一人维护,PR 排队半年,bug 修不完。Naver 的 dataviz 团队决定 fork 不是策略——他们直接用 TypeScript 重写,但保持 API 兼容。
- 2018 年:billboard.js 1.0 发布,主打”无痛迁移 + 类型友好”。
- 2020 年:3.0 引入模块化加载,按需 import 图类型,兼容 D3 v6 ESM。
- 2024 年至今:稳定迭代,新增 sankey / treemap / sunburst 等 c3 没有的图,主题系统扩到四套。
c3 的 issue 区现在仍有人留言”已迁移到 billboard,谢谢作者”——这是开源社区”接力”的典型样本。
学到什么
- API 兼容是迁移项目的护城河——重写引擎但不动方向盘,是最便宜的用户增长策略
- 后继者不一定要”颠覆”前任——平滑接棒比另起炉灶更受存量项目欢迎
- TS 重写不是炫技:类型暴露让 IDE 替文档干活,新人上手时间断崖式下降
- 模块化 + tree-shaking 是现代库的及格线,不做就被 bundle 体积淘汰
延伸阅读
- 官方文档:billboard.js docs(example 极多,每种图都有可改的 live demo)
- 迁移指南:c3 to billboard migration
- chart-js —— 同样定位”易用图表”,但 Canvas 渲染、不依赖 D3
- d3 —— billboard 的底层引擎,看懂 D3 就理解了 billboard 的留白
- echarts —— 同样配置式,但生态体量大十倍,更适合复杂关系图
- recharts —— React 项目首选 SVG 系,对照看 declarative 与 imperative 的差异