Observalble Plot和图形语法理论

Observable Plot 是一个免费的开源 JavaScript 库(https://github.com/observablehq/plot),用于可视化表格数据,专注于加速探索性数据分析。它拥有简洁、易记且表达力强的界面,采用了由 Leland Wilkinson 和 Hadley Wickham 推广的图形语法风格中的刻度(scales)和分层标记(layered marks),并受到 Jacques Bertin 早期理念的启发。此外,还提供了大量可供学习和复制粘贴的示例(https://observablehq.com/)。

但「采用了图形语法风格」到底意味着什么?Observable Plot 实现了图形语法的全部,还是只是一部分?它和 ggplot2 这种「图形语法标杆」有什么区别?本文从图形语法的理论出发,拆解 Observable Plot 的实现,最后讨论图形语法的边界与补齐方案。

一、什么是图形语法

1.1 传统图表库的痛点

传统图表库(如 ECharts、Highcharts)采用「图表类型」思维:你想画散点图就选 type: 'scatter',想画柱状图就选 type: 'bar'。每种图表是一个封装好的黑盒,内部逻辑不可见、不可组合。

这种模式优势显著,有很多成熟的现成图表,但仍然有一些痛点:

  • 组合困难:想在散点图上加一条回归线?需要两个 series 手动配置,数据格式还不一样

  • 扩展困难:想要一个库没有的图表类型?只能等官方更新或自己写插件

  • 概念不统一:每种图表的配置项各搞一套,学习成本高

1.2 图形语法的核心思想

1999 年,Leland Wilkinson 在《The Grammar of Graphics》中提出了一个革命性的思想:一张图不是一种「类型」,而是一组「构件」的组合。就像自然语言中句子由主语、谓语、宾语组合而成,图表由数据、映射、统计变换、几何对象、标尺等构件组合而成。

2010 年,Hadley Wickham 在 ggplot2 中将这一思想落地为「分层图形语法」,核心是九大构件:

构件 作用 类比
Data 输入数据(长表格式) 食材
Aes 数据字段→视觉属性的映射 菜谱(哪样食材放哪)
Stat 统计变换(分箱、平滑、计数) 烹饪加工
Geom 几何对象(点、线、柱) 装盘形状
Scale 数据值→像素值的标尺 计量单位换算
Coord 坐标系(笛卡尔、极坐标) 餐桌形状
Position 位置调整(堆叠、并排、抖动) 摆盘位置
Facet 分面(按字段分组画子图) 分餐制
Theme 主题(颜色、字体、间距) 餐厅装修风格

解耦是图形语法的灵魂。Stat 和 Geom 是解耦的:同一个分箱统计(stat_bin)可以配柱状图(geom_bar),也可以配散点图(geom_point)。同一个平滑统计(stat_smooth)可以配线(geom_line),也可以配置信区间(geom_ribbon)。这种自由组合是传统图表库做不到的。

1.3 渲染流水线

九大构件按固定的流水线协作:

text 复制代码
原始数据
  → stat 统计变换(可选)→ 中间数据
  → aes 映射(确定用哪些字段)
  → scale 映射(数据值 → 像素值)
  → geom 渲染(生成图形元素)
  → position 位置调整(可选)
  → coord 坐标变换(最后一步)
  → 叠加到 layer 中
  → 绘制 axis/legend(基于 scale)
  → theme 样式应用

注意几个关键顺序:Stat 在 Geom 之前(统计变换输出中间数据,Geom 消费中间数据);Coord 在最后(坐标系变换不影响统计计算);Axis/Legend 是 Scale 的副产品。

二、图形语法举例

用一个具体例子来理解九大构件如何协作。数据集是经典的 mtcars(32 款汽车的参数),我们要画一张「车重 vs 油耗」的散点图,并按气缸数着色,再加一条回归线。

2.1 ggplot2 实现

r 复制代码
ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  geom_smooth(method = "lm", se = FALSE, color = "black") +
  scale_x_continuous(name = "车重 (1000 lbs)") +
  scale_y_continuous(name = "油耗 (mpg)") +
  scale_color_brewer(palette = "Set1", name = "气缸数") +
  theme_minimal()

2.2 逐构件拆解

构件 在本例中的作用 具体值
Data 输入数据集 mtcars(32行×11列的长表)
Aes 字段→视觉通道映射 x=wt, y=mpg, color=factor(cyl)
Stat 统计变换 第一层无(原始数据);第二层 stat_smooth(线性回归)
Geom 几何对象 第一层 geom_point(散点);第二层 geom_smooth(线)
Scale 数据→像素映射 x/y 线性标尺,color 用 Set1 色板
Coord 坐标系 默认笛卡尔坐标
Position 位置调整 默认 identity(不调整)
Facet 分面 无(单图)
Theme 主题 theme_minimal(极简风格)

2.3 数据流向

第一层(散点):原始 mtcars 数据 → aes 取出 wt/mpg/cyl → scale 映射成坐标和颜色 → geom_point 生成 32 个 circle。

第二层(回归线):原始 mtcars 数据 → stat_smooth 做线性回归,输出 2 个点 {x, y} → scale 映射成坐标 → geom_smooth 生成 1 条 path。

两层叠加,坐标轴和图例基于 scale 自动生成,主题应用全局样式。这就是图形语法的完整流水线。

2.4 解耦的威力

如果想把这张图改成直方图,只需要改 stat 和 geom:

r 复制代码
# 散点图
ggplot(mtcars, aes(x = mpg)) + geom_point(stat = "bin")

# 直方图(同样的数据,同样的 aes,换 stat + geom)
ggplot(mtcars, aes(x = mpg)) + geom_histogram(bins = 10)

数据、aes、scale 都不变,只换了 stat(bin)和 geom(bar),就从散点图变成了直方图。这就是解耦的表达力。

三、Observable Plot 实现了哪些部件

3.1 Observable Plot 的设计

Observable Plot 的定位是「轻量化图形语法子集」。它不追求实现图形语法的全部,而是在「简洁 API」和「语法完整性」之间做取舍。典型特点如:

  • stat 和 mark 耦合:不区分 stat 和 geom,统一称为 mark(标记)。如 Plot.dot(散点)、Plot.line(折线)、Plot.bin(直方图,内置分箱统计)

  • 函数式 API:每个 mark 是一个函数,返回一个描述对象,传入 Plot.plot() 渲染

  • 原生交互:tooltip、brush、zoom 内置,不需要额外配置

  • 基于 D3:底层用 D3 的 scale、shape、axis,站在巨人肩膀上

3.2 九大构件对应关系

构件 实现度 Observable Plot 对应 说明
Data 完整 data 参数(数组/typed array/列对象) 支持多种数据格式,自动识别
Aes 部分 channels(x/y/fill/stroke/r/opacity...) 通道映射完整,但无独立 aes 抽象,直接写在 mark 里
Stat 部分 内置在 mark 中(bin/interval/hexgrid...) 统计逻辑固化在 mark 中,不可独立插拔,无自定义 stat
Geom 部分 marks(dot/line/bar/area/rect...) 几何对象丰富,但与 stat 耦合,不可自由组合
Scale 完整 scales 配置(x/y/color/opacity...) 基于 D3-scale,支持 linear/log/ordinal/time/band 等
Coord 部分 projection(地理投影)、polar(极坐标,有限) 地理投影强,但极坐标仅支持部分 mark,无 coord_flip/coord_trans
Position 部分 offset 参数(stack/normalize/jitter) 支持堆叠、归一化、抖动,但无 dodge(分组并排需手动处理)
Facet 部分 fx/fy 通道 支持一维/二维分面,但无 free scales,布局灵活性有限
Theme 较完整 style/color/grid 配置 + 预设主题 支持自定义样式,有 light/dark 预设,但不如 ggplot2 精细

3.3 已实现部件详解

Scale(标尺):最完整的构件

Observable Plot 的 scale 体系基于 D3-scale,是九大构件中实现最完整的。支持:

  • 连续型:linear、log、pow、sqrt、symlog、time、utc

  • 离散型:ordinal、band、point、quantile、quantize、threshold

  • 颜色比例尺:categorical、sequential、diverging、cyclic(内置 D3 色板)

  • nice/ticks/label:自动优化刻度和标签

javascript 复制代码
Plot.plot({
  x: { type: "log", label: "车重 (log scale)" },
  y: { grid: true, label: "油耗" },
  color: { type: "ordinal", scheme: "Set1", legend: true },
  marks: [Plot.dot(mtcars, { x: "wt", y: "mpg", fill: "cyl" })]
})

Mark(标记):stat + geom 的耦合体

Observable Plot 不区分 stat 和 geom,统一为 mark。每个 mark 内置了统计逻辑和渲染逻辑:

Mark 内置统计 对应 ggplot2
dot geom_point
line 无(按 x 排序) geom_line
barY/barX geom_col
rect geom_tile
area geom_area
bin 分箱计数 stat_bin + geom_bar
hexgrid 六边形分箱 geom_hex
density 核密度估计 stat_density + geom_density
linearRegression 线性回归 stat_smooth(method="lm")
loessRegression LOESS 回归 stat_smooth(method="loess")
boxplot 箱线图统计 stat_boxplot + geom_boxplot
contour 等高线计算 geom_contour

这种设计的好处是 API 简洁------画直方图只需要 Plot.bin(data, {x: "mpg"}),不需要分别指定 stat 和 geom。代价是失去了 stat-geom 自由组合的能力。

Facet(分面):fx/fy 通道

Observable Plot 用 fx/fy 通道实现分面,非常简洁:

javascript 复制代码
Plot.plot({
  fx: { label: "气缸数" },
  marks: [
    Plot.dot(mtcars, { x: "wt", y: "mpg", fx: "cyl" }),
    Plot.linearRegressionY(mtcars, { x: "wt", y: "mpg", fx: "cyl", stroke: "red" })
  ]
})

但限制是:所有分面共享相同的 scale(无 free scales),布局是规则网格,不支持复杂的自定义布局。

交互:原生支持

这是 Observable Plot 相比 ggplot2 的最大优势------交互是内置的,不需要额外配置:

javascript 复制代码
Plot.plot({
  marks: [
    Plot.dot(mtcars, {
      x: "wt", y: "mpg", fill: "cyl",
      tip: true,  // tooltip,一行配置
      brush: { filter: null }  // 框选
    })
  ]
})

ggplot2 本身是静态的,交互需要通过 plotly::ggplotly() 转换。而 Observable Plot 的 tooltip、brush、zoom 都是原生的。

3.4 Observable Plot 的不足

  1. stat 和 mark 耦合,无自定义 stat:统计逻辑固化在 mark 中,不能像 ggplot2 那样自由组合(如 stat_bin + geom_point)。也不能注册自定义的全局复用 stat。

  2. 无独立 aes 抽象:映射直接写在每个 mark 的参数里,没有全局 aes + 局部 aes 的继承机制。多个 mark 共享映射时需要重复写。

  3. Coord 能力有限:地理投影(projection)很强,但极坐标仅支持部分 mark,没有 coord_flip(x/y 翻转)、coord_trans(坐标变换)。饼图的实现不方便,需要使用 d3 arc。

  4. Position 不完整:支持 stack/normalize/jitter,但没有 dodge(分组柱状图的并排),需要手动计算偏移或用 groupX。

  5. Facet 无 free scales:所有分面共享 x/y scale,不能让每个子图有独立的坐标范围。

  6. 无自定义 geom 机制:不能像 ggplot2 那样编写自定义 Geom 类并注册。自定义图形需要用 Plot.vector 或直接操作 SVG。

  7. 双轴支持弱:没有 sec_axis(次坐标轴),双 y 轴图需要手动处理。

3.5 Observable Plot 的优势

  1. API 极简 :画一张图只需要几行代码。散点图 Plot.dot(data, {x, y}),直方图 Plot.bin(data, {x})。学习成本远低于 ggplot2。

  2. 原生交互:tooltip、brush、zoom 内置,一行配置开启。这是 ggplot2 做不到的。

  3. 基于 D3 生态:scale、shape、axis、projection 都用 D3,质量有保障。可以和 D3 代码混用。

  4. Observable 原生集成:在 Observable Notebook 中零配置使用,响应式更新。也支持 Quarto、R Markdown。

  5. 默认样式好:不需要配置主题就能得到美观的图表,网格线、颜色、字体都有精心设计的默认值。

  6. 类型安全:TypeScript 原生支持,IDE 自动补全。

3.6 同一张图的代码对比

javascript 复制代码
Plot.plot({
  grid: true,
  x: { label: "车重 (1000 lbs)" },
  y: { label: "油耗 (mpg)" },
  color: { legend: true, label: "气缸数" },
  marks: [
    Plot.dot(mtcars, { x: "wt", y: "mpg", fill: "cyl", tip: true }),
    Plot.linearRegressionY(mtcars, { x: "wt", y: "mpg", stroke: "black" })
  ]
})
r 复制代码
ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  geom_smooth(method = "lm", se = FALSE, color = "black") +
  labs(x = "车重 (1000 lbs)", y = "油耗 (mpg)", color = "气缸数") +
  theme_minimal() +
  theme(legend.position = "right")
# 交互需要 plotly::ggplotly() 转换

Observable Plot 代码更短,且原生支持 tooltip。但 ggplot2 的 stat-geom 解耦更清晰,扩展能力更强。

四、延伸:图形语法的边界和解决方案

4.1 图形语法的六大短板

通过前面的分析可以看到,即使是 ggplot2 这种「图形语法标杆」,也有一些固有的短板------这些不是实现问题,而是图形语法本身的设计边界:

短板 原因 典型图表
拓扑布局 图形语法绑定二维表,无节点-边抽象 力导向图、桑基图、树图、和弦图
自由排版 Facet 是规则网格,非自由布局 仪表盘、多图组合、不规则排版
交互 ggplot2 设计为静态输出 tooltip、brush、zoom、下钻
3D 图形语法本质是 2D 的 3D 散点、3D 曲面、3D 柱状
地图 需要地理投影和空间数据结构 choropleth、飞线、热力地图
大数据性能 SVG/grid 渲染在万级以上卡顿 百万级散点、实时流数据

4.2 各短板的解决方案

拓扑布局:专用图可视化引擎

图形语法不擅长节点-边关系图,因为这类图需要布局算法(力导向、树布局、桑基布局),而不是简单的 x/y 映射。解决方案:

  • AntV G6:专业的图可视化引擎,内置力导向、树、DAG、桑基等布局,支持自定义节点和边

  • D3-force / D3-sankey / D3-hierarchy:D3 的布局算法模块,可以和 D3 的渲染配合

  • Cytoscape.js:生物信息学领域常用的网络图库

  • ECharts graph/sankey/tree:内置拓扑布局的系列配置

  • ggplot2 + ggraph:ggplot2 的扩展包,用图形语法画网络图(但布局仍需外部算法)

自由排版:组合布局工具

图形语法的 facet 是规则网格,不适合复杂的仪表盘布局。解决方案:

  • patchwork(R):ggplot2 的多图组合包,支持自定义布局网格

  • cowplot(R):另一个 ggplot2 组合工具

  • CSS Grid / Flexbox(Web):用 CSS 布局多个图表容器

  • ECharts grid:多 grid 布局,支持任意位置和大小的子图

交互:Web 原生或转换

  • Observable Plot / Vega-Lite / AntV G2:Web 端图形语法库,原生支持交互

  • plotly::ggplotly():将 ggplot2 转换为交互式 Plotly 图表

  • shiny(R):用 R 做交互式可视化应用

  • D3.js:完全自定义交互

3D:专用 3D 引擎

  • Three.js:Web 端 3D 渲染的事实标准

  • Plotly 3D:支持 3D 散点、曲面、柱状

  • ECharts GL:ECharts 的 3D 扩展

  • rgl(R):R 的 3D 可视化包

地图:地理投影库

  • D3-geo:D3 的地理投影模块,支持 100+ 投影

  • Observable Plot projection:内置地理投影支持

  • ECharts map:内置地图注册、飞线、热力

  • ggplot2 + sf/ggspatial:R 的空间数据扩展

大数据性能:Canvas/WebGL 渲染

  • WebScatter / Deck.gl:WebGL 渲染,支持百万级数据点

  • ECharts large 模式:Canvas 增量渲染

  • Datashader(Python):服务端预渲染大数据

  • Observable Plot raster:栅格化渲染,提升大数据性能

4.3 组合使用是最佳实践

没有一个库能搞定所有场景。实际项目中通常是组合使用:

  • 数据分析阶段:用 ggplot2/plotnine 做探索性分析(图形语法的灵活性最高)

  • Web 展示阶段:统计图表用 Observable Plot/G2,业务大屏用 ECharts,网络图用 G6

  • 自定义需求:用 D3.js 实现图形语法库不支持的图表

  • 大数据/3D:用 Deck.gl/Three.js 等专用引擎

总结

Observable Plot 是图形语法的「轻量化子集」------它完整实现了 Scale(标尺)和 Layered Marks(分层标记),但将 Stat 和 Geom 耦合为 Mark,牺牲了自由组合能力换取了 API 的简洁。它的优势是极简的 API、原生交互、D3 生态和 Observable 集成;不足是无自定义 stat/geom、Coord/Position/Facet 能力有限。

图形语法本身也有边界------拓扑布局、自由排版、3D、地图、大数据性能等场景需要专用库补齐。理解这些边界,才能在合适的场景选择合适的工具。

相关推荐
开开心心就好13 分钟前
手机悬屏翻译工具外语游戏漫画APP全覆盖
android·前端·javascript·python·游戏·pdf·html
支支დ20 分钟前
Phind 前端特定优势:AI 编程助手的体验革新
前端
DDXYcoder25 分钟前
万字解析动态内存从编译到运行
java·前端·算法
光影少年26 分钟前
react navite封装一个RN 通用按钮组件
前端·javascript·react native·react.js·前端框架
晴天1628 分钟前
CSS 预处理器深度解析-Day35
前端·css
恋猫de小郭30 分钟前
AI 时代,也许你的 Flutter 需要一套 Dartastic OpenTelemetry 监控
android·前端·flutter
wujiuhsu33 分钟前
前端实现二维码生成器:Canvas、SVG、纠错等级、扫码校验与批量
前端
计算机魔术师38 分钟前
从卖铲人到圈地:英伟达129亿拿下Hugging Face
前端
攻城狮-申41 分钟前
git本地分支对齐远程分支
前端·git