可视化技术复盘:从图形语法看 D3、Vega-Lite、ECharts、Observable Plot

可视化技术复盘:从图形语法看 D3、Vega-Lite、ECharts、Observable Plot

写在前面

做可视化应用这些年,踩过不少选型的坑。以前习惯上来就搜"ECharts 和 D3 哪个更强",看一堆 Demo 就直接引入项目。等到要做复合自定义图表、拖拽BI画布的时候,才发现底层模型不匹配,返工成本很高。

本文是结合多个可视化、BI编辑器项目的个人复盘,仅代表个人实践理解,不存在绝对标准答案,欢迎评论区交流讨论。

很多人接触可视化,是从 Excel、ECharts 的图表下拉列表开始:要柱状图就点柱状图,要饼图就点饼图。

这是一种枚举式思维:把每一类图表当成独立模板,每新增一种图表,就要新增一套绘图逻辑、一套配置面板。

但图形语法《The Grammar of Graphics》提供了一套完全相反的思路:图表不是预定义好的模板菜单,而是由一组互相正交的基础组件拼装出来的产物

就像造句不是背现成句子,而是用字、词、语法规则自由组合;图表也是一样,不同的图元、通道映射、变换、坐标系互相组合,就可以生成已知、甚至从来没有命名过的图表。

正交:各个组件互相独立,修改其中一个,不会强制改动其他部分。比如同样一套折线图映射,只切换坐标系,就直接变成雷达图,其余配置不用大改。

一、图表的四大基础构件

一张统计图表,可以拆解成4个核心构件:

  1. Mark(图元/几何对象):最小绘图单元,矩形、圆点、线条、弧形、路径、文本。
  2. Encoding(视觉通道映射):把数据表字段映射到视觉属性,x/y位置、角度theta、半径radius、颜色、大小、形状。
  3. Transform(数据变换/统计变换):聚合、分组、堆叠、分箱、层级布局、过滤,绘图前重塑原始数据。
  4. Coordinate(坐标系) :笛卡尔直角坐标系、极坐标系;同一套mark+encoding,换坐标系,图表形态直接大变

核心公式:图表 = Mark + Encoding + Transform + Coordinate

26种常见图表拆解举例,换种图形思考方式

复制代码
柱状图 = 矩形(bar)图元 + X【分类维度映射】 + Y【数值映射】
条形图 = 矩形(bar)图元 + Y【分类维度映射】 + X【数值映射】
# ✅ 仅仅交换X/Y通道映射,不换mark,柱状图直接变成条形图!

折线图 = 线条(line)图元 + X【时间/有序维度映射】 + Y【数值映射】
面积图 = 面积(area)图元 + X【时间维度映射】 + Y【数值映射】
# ✅ mark从line换成area,其余映射不变,折线图变面积图

散点图 = 圆点(point)图元 + X【数值映射】 + Y【数值映射】+ 颜色/大小【额外通道编码】
气泡图 = 圆点(point)图元 + X【数值映射】 + Y【数值映射】+ size【数值映射】
# ✅ 散点图增加size通道,直接升级气泡图

饼图 = 弧形(arc)图元 + theta【角度通道映射】 + radius【半径】
# ✅ 坐标系从笛卡尔直角坐标系,转换成极坐标系

堆叠柱状图 = 矩形(bar)图元 + X【分类】 + Y【数值】 + color【分组字段】+ stack堆叠变换
# ✅ 新增颜色通道+stack transform,普通柱状图→堆叠柱状图

箱线图 = boxplot图元 + X【分类】 + Y【数值分布映射】
热力图 = 矩形(rect)图元 + X【维度】 + Y【维度】 + color【数值通道映射】
# ✅ 热力图依然是矩形图元,只是Y不再是高度,而是二维网格位置,数值映射到颜色

南丁格尔玫瑰图 = 弧形(arc)图元 + theta【分类角度映射】 + radius【数值半径映射】
# ✅ 极坐标系,半径映射数值,角度分配分类;饼图是固定半径,玫瑰图把数值绑定到半径通道

旭日图 = 弧形(arc)图元 + theta【角度映射】 + radius【层级深度映射】 + color【分类】 + hierarchy层级变换
# ✅ 极坐标,多层嵌套arc,transform做层级聚合,内圈父级外圈子级

树形图(Treemap) = 矩形(rect)图元 + size【数值面积映射】 + color【分类】 + treemap分块变换
# ✅ 直角坐标系,不再用x/y位置编码主数值,用矩形面积表达指标大小

桑基图 = 路径(path)图元 + X【节点层级映射】 + Y【流量权重映射】 + color【分组】 + sankey流向变换
# ✅ 自定义路径图元,transform计算节点位置与流量宽度,表达跨节点流转

弦图(Chord) = 弧形+路径(path)图元 + theta【极坐标角度】 + radius【半径】 + chord关联变换
# ✅ 极坐标系,arc代表节点,path弦代表节点之间的关联流量

K线图 = 规则线段/矩形(rule+bar)复合图元 + X【时间维度映射】 + Y【高低价数值映射】
# ✅ 复合mark,竖线映射最高最低价,矩形映射开盘收盘价;单套x时间通道绑定整套K线

雷达图 = 线条(line)+点(point)复合图元 + theta【维度角度映射】 + radius【指标数值映射】
# ✅ 极坐标,每个角度代表一个指标维度,半径代表指标得分

瀑布图 = 矩形(bar)图元 + X【分类】 + Y【累计数值映射】 + 偏移基线transform
# ✅ 本质柱状图,transform动态计算每个柱子的基线起点,有正负浮动

范围区间图(区间面积图) = area图元 + X【有序维度】 + y【下限数值】 + y2【上限数值】
# ✅ 增加y2通道,一条线改成上下两条边界填充面积

阶梯图 = line图元 + X【有序维度】 + Y【数值】 + step插值变换
# ✅ mark依然是line,仅修改插值模式,折线变成阶梯折线

回归线 = line图元 + X【数值】 + Y【数值】 + regression回归transform
# ✅ 原始散点数据,transform做线性回归拟合,输出拟合线条

词云 = 文本(text)图元 + text【文本字段】 + size【词频数值映射】 + 布局碰撞变换
# ✅ mark是文字,size通道绑定词频,transform负责文字排布防重叠

误差棒图 = rule线段图元 + X【分类】 + Y【均值】 + yError【误差范围通道】
# ✅ 在主mark基础上增加误差通道,自动上下延伸误差线段

马赛克图 = 矩形(rect)图元 + X【分类A】 + Y【分类B】 + color【频次数值】 + 分块transform
# ✅ 二维分类网格,矩形面积代表样本数量,和热力图区别:面积参与编码

棒棒糖图 = point + rule复合图元 + X【数值】 + Y【分类】
# ✅ 圆点+连接线,mark组合,和条形图同源,把实心矩形替换成点+线

甘特图 = 矩形(bar)图元 + X【起始时间】 + x2【结束时间】 + Y【任务分类】
# ✅ bar图元,用x/x2通道标记时间起止,Y轴放任务名称

等值线图(等高线) = path图元 + X【x坐标】 + Y【y坐标】 + color【数值】 + contour等高线变换
# ✅ transform基于二维网格生成等值路径,path渲染等高线条纹

蜂巢图(Hexbin六边形分箱) = 六边形(hexagon)图元 + X【数值】 + Y【数值】 + color【计数聚合】 + hexbin分箱transform
# ✅ 散点数据经过分箱聚合,六边形图元,颜色映射箱内样本数量

连接散点图(连线散点) = point+line复合图元 + X【有序维度】 + Y【数值】 + order排序通道
# ✅ 散点基础上叠加line,按order通道连接点,也就是带连线的散点图

极坐标柱状图(径向柱状图) = bar图元 + theta【分类角度】 + radius【数值长度】,极坐标系
# ✅ 普通柱状图,直接切换坐标系为极坐标,柱子从竖直变成放射状

分层树状图(Tree) = point + path图元 + X/Y位置 + hierarchy层级变换
# ✅ transform计算树节点布局,path绘制父子连接线,point渲染节点

面积堆叠图 = area图元 + X【有序维度】 + Y【数值】 + color分组 + stack堆叠变换
# ✅ 基础面积图,增加stack transform,多层面积向上堆叠

双向条形图(人口金字塔) = bar图元 + Y【分类】 + X【正负数值】 + color分组
# ✅ 条形图,X支持正负,基线在中间,左右双向延伸

多系列折线图 = line图元 + X【有序维度】 + Y【数值】 + color【系列分组】
# ✅ 单line mark,color通道做分组,自动生成多条独立折线

符号图(形状编码散点) = point图元 + X【数值】 + Y【数值】 + shape【分类通道】
# ✅ 散点基础上新增shape视觉通道,不同分类自动渲染圆形、三角、方块

折线+柱状组合图 = bar + line 多层mark + 共享X维度通道,两套独立Y数值通道
# ✅ 多layer,共用x编码,bar绑定左Y,line绑定右Y,双轴复合图表

核心公式:图表 = Mark + Encoding + Transform + Coordinate列成表格更直观一些:

当然,图形语法有很现实的矛盾,直接给终端用户用,门槛太高,不像看图就选那么直接。

所以哪怕是深受图形语法启发的 Tableau,对外依然提供一大堆预制图表类型菜单。底层内核用抽象模型,上层产品依旧用枚举模板做用户妥协。理论强大 ≠ 产品好用。

二、两大工程实现路线(个人观察)

图形语法是一套思想,不是某一个库的专利。不同框架落地的时候,分化成两条路线。
#mermaid-svg-CngHaAVai6fPp7g8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CngHaAVai6fPp7g8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CngHaAVai6fPp7g8 .error-icon{fill:#552222;}#mermaid-svg-CngHaAVai6fPp7g8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CngHaAVai6fPp7g8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CngHaAVai6fPp7g8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CngHaAVai6fPp7g8 .marker.cross{stroke:#333333;}#mermaid-svg-CngHaAVai6fPp7g8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CngHaAVai6fPp7g8 p{margin:0;}#mermaid-svg-CngHaAVai6fPp7g8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CngHaAVai6fPp7g8 .cluster-label text{fill:#333;}#mermaid-svg-CngHaAVai6fPp7g8 .cluster-label span{color:#333;}#mermaid-svg-CngHaAVai6fPp7g8 .cluster-label span p{background-color:transparent;}#mermaid-svg-CngHaAVai6fPp7g8 .label text,#mermaid-svg-CngHaAVai6fPp7g8 span{fill:#333;color:#333;}#mermaid-svg-CngHaAVai6fPp7g8 .node rect,#mermaid-svg-CngHaAVai6fPp7g8 .node circle,#mermaid-svg-CngHaAVai6fPp7g8 .node ellipse,#mermaid-svg-CngHaAVai6fPp7g8 .node polygon,#mermaid-svg-CngHaAVai6fPp7g8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CngHaAVai6fPp7g8 .rough-node .label text,#mermaid-svg-CngHaAVai6fPp7g8 .node .label text,#mermaid-svg-CngHaAVai6fPp7g8 .image-shape .label,#mermaid-svg-CngHaAVai6fPp7g8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CngHaAVai6fPp7g8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CngHaAVai6fPp7g8 .rough-node .label,#mermaid-svg-CngHaAVai6fPp7g8 .node .label,#mermaid-svg-CngHaAVai6fPp7g8 .image-shape .label,#mermaid-svg-CngHaAVai6fPp7g8 .icon-shape .label{text-align:center;}#mermaid-svg-CngHaAVai6fPp7g8 .node.clickable{cursor:pointer;}#mermaid-svg-CngHaAVai6fPp7g8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CngHaAVai6fPp7g8 .arrowheadPath{fill:#333333;}#mermaid-svg-CngHaAVai6fPp7g8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CngHaAVai6fPp7g8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CngHaAVai6fPp7g8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CngHaAVai6fPp7g8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CngHaAVai6fPp7g8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CngHaAVai6fPp7g8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CngHaAVai6fPp7g8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CngHaAVai6fPp7g8 .cluster text{fill:#333;}#mermaid-svg-CngHaAVai6fPp7g8 .cluster span{color:#333;}#mermaid-svg-CngHaAVai6fPp7g8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CngHaAVai6fPp7g8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CngHaAVai6fPp7g8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CngHaAVai6fPp7g8 .icon-shape,#mermaid-svg-CngHaAVai6fPp7g8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CngHaAVai6fPp7g8 .icon-shape p,#mermaid-svg-CngHaAVai6fPp7g8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CngHaAVai6fPp7g8 .icon-shape .label rect,#mermaid-svg-CngHaAVai6fPp7g8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CngHaAVai6fPp7g8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CngHaAVai6fPp7g8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CngHaAVai6fPp7g8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 思路1:语法原生落地
思路2:业务模板封装
图形语法理论
原子组装派
模板优先派
无固定图表类型

自由组合mark/通道/变换
擅长:自定义复合图、BI编辑器、动态模板
代表:Vega‑Lite、Observable Plot、ggplot2
预设高频图表模板

屏蔽底层组装细节
擅长:业务系统、大屏、标准图表
代表:ECharts、Highcharts

原子组装派(Vega‑Lite / Observable Plot / ggplot2)

核心:mark、encoding、transform、坐标系是一等公民。

没有顶层"柱状图""饼图"模板概念,图表是拼装的结果。

  • 优点:改动单一组件就可以变换图表形态;图层自由叠加;配置完整可序列化,非常适合拖拽式BI编辑器。
  • 缺点:简单业务图表,也要理解这套抽象概念,有学习成本。

模板优先派(ECharts / Highcharts)

核心:把高频的「mark+通道+变换+坐标系」预先封装为series.type

底层其实存在mark、通道映射的逻辑,但被模板封装隐藏起来。

  • 优点:开箱即用,tooltip、图例、交互、自适应全部封装,业务开发效率极高。
  • 缺点:模板一旦选定,内部组件被锁定;想要动态混搭图元、切换底层映射,改动代价很大。

举个直观差异:

Vega‑Lite:交换x与y的encoding,柱状图直接变条形图。

ECharts:柱状和条形虽然type都是bar,但需要同时修改xAxis、yAxis整套配置,不是简单交换字段映射。

三、同一套数据,四大库代码横向对照

测试数据源:

json 复制代码
[
  {"category":"A","sales":28},
  {"category":"B","sales":55},
  {"category":"C","sales":42},
  {"category":"D","sales":33}
]

1. Vega‑Lite(原子组装派 JSON声明式)

json 复制代码
{
  "$schema": "https://vega.github.io/schema/vega‑lite/v5.json",
  "data": {
    "values": [
      {"category":"A","sales":28},
      {"category":"B","sales":55},
      {"category":"C","sales":42},
      {"category":"D","sales":33}
    ]
  },
  "mark": {"type":"bar"},
  "encoding": {
    "x": {"field":"category","type":"nominal"},
    "y": {"field":"sales","type":"quantitative"}
  }
}

映射关系:

  • mark:bar 对应Mark图元
  • encoding.x / encoding.y 对应Encoding视觉通道
  • scale、坐标轴、图例自动推断生成。

👉 只交换encoding内x、y,不需要修改mark,直接得到条形图。

👉 只修改mark.type="line",保持encoding不变,直接得到折线图。

演示直角柱状图切换极坐标径向柱状图,mark依旧为bar,仅替换通道+开启极坐标

json 复制代码
{
  "$schema": "https://vega.github.io/schema/vega‑lite/v5.json",
  "data": {
    "values": [
      {"category":"A","sales":28},
      {"category":"B","sales":55},
      {"category":"C","sales":42},
      {"category":"D","sales":33}
    ]
  },
  "mark": {"type":"bar"},
  "encoding": {
    "theta": {"field":"category","type":"nominal"},
    "radius": {"field":"sales","type":"quantitative"}
  },
  "view": {"stroke":null},
  "coordinate": {"type":"polar"}
}

2. Observable Plot(原子组装派 JS函数式)

js 复制代码
const data = [
  {"category":"A","sales":28},
  {"category":"B","sales":55},
  {"category":"C","sales":42},
  {"category":"D","sales":33}
];

Plot.plot({
  marks: [
    Plot.barY(data, {x:"category", y:"sales"})
  ]
})
  • Plot.barY封装bar mark;参数x/y就是encoding映射。
  • 切换条形图:直接改为 Plot.barX(data, {y:"category", x:"sales"})

3. ECharts(模板优先派)

js 复制代码
const option = {
  xAxis: { type: 'category' },
  yAxis: { type: 'value' },
  series: [
    {
      type: 'bar',
      data: [
        {name:"A", value:28},
        {name:"B", value:55},
        {name:"C", value:42},
        {name:"D", value:33}
      ]
    }
  ]
}
  • series.type:'bar':封装完整的bar整套逻辑。
  • 想要切换为散点,不能只改type字符串,需要新增一套独立scatter series
  • 聚合、分组等Transform逻辑,框架没有内置声明式实现,需要业务JS预先处理数据。

4. D3.js(底层原语,自己实现图形语法)

D3本身不提供任何图形语法封装,只有比例尺、DOM操作、布局工具。mark、encoding、scale全部需要开发者手写实现。

js 复制代码
const data = [
  {"category":"A","sales":28},
  {"category":"B","sales":55},
  {"category":"C","sales":42},
  {"category":"D","sales":33}
];

//手动创建比例尺 Scale
const xScale = d3.scaleBand()
  .domain(data.map(d=>d.category))
  .range([0, 400])
  .padding(0.2);

const yScale = d3.scaleLinear()
  .domain([0, 60])
  .range([200, 0]);

//手动绘制Mark矩形,手动做Encoding绑定
d3.select("svg")
  .selectAll("rect")
  .data(data)
  .join("rect")
  .attr("x", d => xScale(d.category))
  .attr("y", d => yScale(d.sales))
  .attr("width", xScale.bandwidth())
  .attr("height", d => 200 - yScale(d.sales));

D3自由度天花板最高,代价:坐标轴、图例、tooltip、统计变换全部需要手写;想把柱状改成条形,要修改比例尺和rect全套属性。

四、数据变换transform的差异:明细数据聚合案例

原始明细数据,同一category有多条记录,需要求和之后绘图:

json 复制代码
[
  {"category":"A","sales":10},
  {"category":"A","sales":18},
  {"category":"B","sales":55},
  {"category":"C","sales":42},
  {"category":"D","sales":33}
]

Vega‑Lite:transform写在spec内部

json 复制代码
{
  "$schema": "https://vega.github.io/schema/vega‑lite/v5.json",
  "data": {
    "values": [
      {"category":"A","sales":10},
      {"category":"A","sales":18},
      {"category":"B","sales":55},
      {"category":"C","sales":42},
      {"category":"D","sales":33}
    ]
  },
  "transform": [
    {"aggregate":[{"op":"sum","field":"sales","as":"total"}], "groupby":["category"]}
  ],
  "mark": {"type":"bar"},
  "encoding": {
    "x": {"field":"category","type":"nominal"},
    "y": {"field":"total","type":"quantitative"}
  }
}

聚合逻辑属于图表描述的一部分,整个spec可以完整序列化保存,适合BI模板存储。

ECharts:变换逻辑放在配置外面手写JS

js 复制代码
const rawData = [
  {"category":"A","sales":10},
  {"category":"A","sales":18},
  {"category":"B","sales":55},
  {"category":"C","sales":42},
  {"category":"D","sales":33}
];
// JS手写聚合
const groupResult = Object.values(rawData.reduce((acc,cur)=>{
  if(!acc[cur.category]) acc[cur.category] = {name:cur.category, value:0};
  acc[cur.category].value += cur.sales;
  return acc;
},{}));

const option = {
  xAxis: {type:"category"},
  yAxis: {type:"value"},
  series: [{type:"bar", data:groupResult}]
}

聚合逻辑不在option配置体内,如果要做拖拽编辑器,这部分业务逻辑需要额外单独存储。

五、复合图层实战:柱状图叠加红色标记最大值点

Vega‑Lite(layer图层复用encoding)

json 复制代码
{
  "$schema": "https://vega.github.io/schema/vega‑lite/v5.json",
  "data": {
    "values": [
      {"category":"A","sales":28},
      {"category":"B","sales":55},
      {"category":"C","sales":42},
      {"category":"D","sales":33}
    ]
  },
  "layer": [
    {
      "mark":"bar",
      "encoding": {
        "x": {"field":"category","type":"nominal"},
        "y": {"field":"sales","type":"quantitative"}
      }
    },
    {
      "mark":{"type":"point","color":"red","size":100},
      "encoding": {
        "x": {"field":"category"},
        "y": {"field":"sales"}
      },
      "transform":[{"filter":"datum.sales == 55"}]
    }
  ]
}

两层layer可以共享同一套通道映射;第二层内部自带filter过滤,不需要外部预处理数据集。

ECharts(新增独立series)

js 复制代码
const data = [
  {name:"A", value:28},
  {name:"B", value:55},
  {name:"C", value:42},
  {name:"D", value:33}
];
const maxPoint = [{name:"B", value:55}];

const option = {
  xAxis: { type: 'category' },
  yAxis: { type: 'value' },
  series: [
    { type: 'bar', data: data },
    { type: 'scatter', symbolSize:10, itemStyle:{color:'red'}, data: maxPoint }
  ]
}

叠加图层靠新增series实现;数据集需要外部单独构造,各series之间没有统一的encoding共享机制。

六、能力横向对比表

能力维度 ECharts Vega‑Lite Observable Plot D3
图形语法规范性 弱,模板封装 极强,标准实现 强,函数式标准 无,仅底层原语
自由混搭图层 弱,新增series 极强,多layer叠加 强,多marks数组 极强,手动绘制
内置声明式数据变换 无,前端JS预处理 聚合/分箱/堆叠等内置 内置常用transform 无,自己手写JS
图表类型动态切换 成本高,切换series模板 低成本,修改mark/encoding 低成本,修改mark函数 成本最高,重写绘制逻辑
适合拖拽编辑器 不适合 最适合 适合 不适合
业务开发效率 极高 中等 较高
自定义艺术图形 极强

七、选型决策树

#mermaid-svg-aogPNPTEnaTpWL6s{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aogPNPTEnaTpWL6s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aogPNPTEnaTpWL6s .error-icon{fill:#552222;}#mermaid-svg-aogPNPTEnaTpWL6s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aogPNPTEnaTpWL6s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aogPNPTEnaTpWL6s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aogPNPTEnaTpWL6s .marker.cross{stroke:#333333;}#mermaid-svg-aogPNPTEnaTpWL6s svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aogPNPTEnaTpWL6s p{margin:0;}#mermaid-svg-aogPNPTEnaTpWL6s .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aogPNPTEnaTpWL6s .cluster-label text{fill:#333;}#mermaid-svg-aogPNPTEnaTpWL6s .cluster-label span{color:#333;}#mermaid-svg-aogPNPTEnaTpWL6s .cluster-label span p{background-color:transparent;}#mermaid-svg-aogPNPTEnaTpWL6s .label text,#mermaid-svg-aogPNPTEnaTpWL6s span{fill:#333;color:#333;}#mermaid-svg-aogPNPTEnaTpWL6s .node rect,#mermaid-svg-aogPNPTEnaTpWL6s .node circle,#mermaid-svg-aogPNPTEnaTpWL6s .node ellipse,#mermaid-svg-aogPNPTEnaTpWL6s .node polygon,#mermaid-svg-aogPNPTEnaTpWL6s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aogPNPTEnaTpWL6s .rough-node .label text,#mermaid-svg-aogPNPTEnaTpWL6s .node .label text,#mermaid-svg-aogPNPTEnaTpWL6s .image-shape .label,#mermaid-svg-aogPNPTEnaTpWL6s .icon-shape .label{text-anchor:middle;}#mermaid-svg-aogPNPTEnaTpWL6s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aogPNPTEnaTpWL6s .rough-node .label,#mermaid-svg-aogPNPTEnaTpWL6s .node .label,#mermaid-svg-aogPNPTEnaTpWL6s .image-shape .label,#mermaid-svg-aogPNPTEnaTpWL6s .icon-shape .label{text-align:center;}#mermaid-svg-aogPNPTEnaTpWL6s .node.clickable{cursor:pointer;}#mermaid-svg-aogPNPTEnaTpWL6s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aogPNPTEnaTpWL6s .arrowheadPath{fill:#333333;}#mermaid-svg-aogPNPTEnaTpWL6s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aogPNPTEnaTpWL6s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aogPNPTEnaTpWL6s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aogPNPTEnaTpWL6s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aogPNPTEnaTpWL6s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aogPNPTEnaTpWL6s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aogPNPTEnaTpWL6s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aogPNPTEnaTpWL6s .cluster text{fill:#333;}#mermaid-svg-aogPNPTEnaTpWL6s .cluster span{color:#333;}#mermaid-svg-aogPNPTEnaTpWL6s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aogPNPTEnaTpWL6s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aogPNPTEnaTpWL6s rect.text{fill:none;stroke-width:0;}#mermaid-svg-aogPNPTEnaTpWL6s .icon-shape,#mermaid-svg-aogPNPTEnaTpWL6s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aogPNPTEnaTpWL6s .icon-shape p,#mermaid-svg-aogPNPTEnaTpWL6s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aogPNPTEnaTpWL6s .icon-shape .label rect,#mermaid-svg-aogPNPTEnaTpWL6s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aogPNPTEnaTpWL6s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aogPNPTEnaTpWL6s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aogPNPTEnaTpWL6s :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是







可视化需求
是否标准业务图表?
选 ECharts
是否需要动态拼装/编辑器/模板?
是否需要可存储JSON规范?
选 Vega‑Lite
选 Observable Plot
是否是非常规自定义图形/特殊布局?
选 D3

场景细化参考

  1. 后台系统、报表、大屏、固定业务图表 → ECharts。开箱即用,文档生态完善。缺点复杂变换要手写JS,不适合拖拽动态拼装。
  2. BI平台、拖拽出图、图表模板存储 → Vega‑Lite。mark+encoding+transform全部存在JSON,完整描述图表。
  3. 自定义复合图层,不想维护繁琐JSON → Observable Plot。保留图形语法思想,JS函数式写法。
  4. 网络图、树图、艺术信息图、高度非常规图形 → D3。完全自主控制渲染,开发成本最高。

八、个人思考总结

  1. 图形语法是一套思维模型,不是Vega专属专利。ECharts内部也存在mark、通道映射的概念,只是被封装在series模板后面。所以各个图表库通过不同的封装和默认值完成了图形语法的不同构件,仅需用户提供少量的信息。
  2. 原子组装派(Vega‑Lite / Plot),mark、encoding、transform是一等公民,动态修改图表形态非常顺滑,代价是存在一定学习门槛。
  3. 模板优先派(ECharts)把高频组合固化,业务开发效率拉满;但动态自由组合的能力会被牺牲。
  4. D3只提供底层绘图原语,图形语法的整套逻辑全部需要开发者自己手写实现,自由度最高,工作量也最大。
  5. 很重要的一点:理论模型强大 ≠ 产品就好用。正交组件虽然在研发层面威力巨大,但直接暴露给普通终端用户会门槛过高,所以很多BI产品底层用图形语法思想,上层依旧提供预制图表菜单做妥协。

没有哪个库是万能的,不存在绝对的孰优孰劣,选型本质是权衡:业务效率、自定义能力、可序列化、维护成本之间做取舍。

以上全部为个人学习总结,认知有限,欢迎指正。

--

相关推荐
尾善爱看海8 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
小弥儿9 小时前
GitHub今日热榜 | 2026-09-15:AI 智能体与本地生成工具领跑
学习·开源
驳是9 小时前
一个组件,提升 react-router 开发的幸福感
前端·preact
susplus9 小时前
【初识ARM】ARM相关基础概念
学习·arm
GreenTea9 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法
星云API技术支持9 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
XiHongShi20169 小时前
键盘大小写指示器(代替键盘灯)
学习·计算机
京东云开发者10 小时前
81.8 秒的视频,我们改了 15 个版本:一次纯 Codex 驱动的 AI-native 视频实践
前端·aigc
Csvn10 小时前
TypeScript 大型项目架构:从单体 tsconfig 到分层可扩展的工程
前端