# moon_vegalite

[English](README.md)

用 MoonBit 写的、由规格驱动的 [Vega-Lite](https://vega.github.io/vega-lite/) 渲染器。
输入一段 Vega-Lite JSON 规格，输出与后端无关的**场景图（scenegraph）**和一份 **SVG**。
支持 `wasm-gc`、`js`、`native` 三个后端；库本身只依赖 MoonBit 核心库和
（手绘风格用到的）[moon_rough](https://github.com/sayoi7799/moon_rough)（rough.js 的移植）；只有命令行工具 `vl2svg` 为了读写文件用到了 `moonbitlang/x`。

## 快速开始

输入 Vega-Lite 规格（JSON 文本），输出 SVG 文档
（完整可运行的工程见 [`examples/hello`](examples/hello)）：

```moonbit
let spec : String =
  #|{
  #|  "data": {"values": [{"a": "A", "b": 28}, {"a": "B", "b": 55}, {"a": "C", "b": 43}]},
  #|  "mark": "bar",
  #|  "encoding": {
  #|    "x": {"field": "a", "type": "nominal"},
  #|    "y": {"field": "b", "type": "quantitative"}
  #|  }
  #|}

fn main {
  let svg = @moon_vegalite.render_svg(spec) catch {
    e => {
      println("error: " + e.message())   // 遇到不支持的特性时，错误信息会给出 JSON 路径
      return
    }
  }
  println(svg)
}
```

```sh
git clone https://github.com/sayoi7799/moon_vegalite
cd moon_vegalite/examples/hello
moon run cmd > bar.svg                     # 用浏览器打开 bar.svg
```

用本仓库的命令行工具 `vl2svg` 画同一张图：

```sh
cd moon_vegalite
moon run cmd/vl2svg --target native -- examples/bar_basic.vl.json bar.svg   # 写出 SVG 文件（不给第二个参数则输出到标准输出）
moon run cmd/vl2svg --target native -- --scene examples/bar_basic.vl.json   # 输出场景图 JSON，含每个图元的包围盒
```

库的接口（`moon_vegalite.mbt`）：

```moonbit
let svg = @moon_vegalite.render_svg(spec_json)        // SVG 文档
let c = @moon_vegalite.compile(spec_json)             // 场景图 + 视图信息
let scene = @moon_vegalite.scene_json(c)              // 场景图的 JSON 形式
```

### 安装

环境要求：[MoonBit 工具链](https://www.moonbitlang.com/download/)（开发和测试使用 `moon 0.1.20261016`）。

模块 `sayoi7799/moon_vegalite` 目前还没有发布到 mooncakes.io，所以通过 `moon.work` 工作区使用本地检出的源码。
在你自己的模块里这样写（完整目录结构见 [`examples/hello`](examples/hello)）：

```
// moon.mod
import {
  "sayoi7799/moon_vegalite@0.2.0",
}
```

```
// moon.work，和你的 moon.mod 放在一起
members = [
  ".",
  "../moon_vegalite",   // 本仓库的检出目录
]
```

```
// 你的包的 moon.pkg
import {
  "sayoi7799/moon_vegalite",
}
```

仓库：[GitHub](https://github.com/sayoi7799/moon_vegalite) · [GitLink](https://gitlink.org.cn/sayoi7799/moon_vegalite)。

## 手绘风格

图表里的图形可以交给 [moon_rough](https://github.com/sayoi7799/moon_rough)（rough.js 4.6.6 的 MoonBit 移植）来画：
矩形、线、路径和符号变成带手绘抖动和斜线填充的草图风格，**文字保持不变**。

```moonbit
let svg = @moon_vegalite.render_svg_sketch(
  spec_json,
  options=@moon_vegalite.SketchOptions::new(seed=42, roughness=1.5, fill_style=CrossHatch),
)
```

```sh
vl2svg --sketch --seed 42 --roughness 1.5 spec.vl.json chart.svg      # 再加 --decimals 2 可以让文件小很多
```

`SketchOptions`：`seed`、`roughness`、`bowing`、`fill_style`（默认 `Hachure`，还有 `Solid`、`Zigzag`、`CrossHatch`、`Dots`、`Dashed`、
`ZigzagLine`）、`hachure_gap`、`fill_weight` 和 `fixed_decimals`（对坐标取整，SVG 小很多）。

| 场景图图元 | 交给 rough 的方式 |
| --- | --- |
| `rect`（柱、格子、图例色块）和绘图区的边框 | `rectangle` |
| `rule`（坐标轴线、刻度、网格线、rule 标记） | `line` |
| `line`、`area`、`symbol`（点、圆、方块等） | `path`，符号保留原来的变换 |
| `text` | 不动 |
| 渐变填充（连续颜色的图例条）、整张图的背景 | 不动 |

只有填充没有描边的图形（比如柱子）会得到一圈和填充同色的轮廓。透明度保留。

**可复现。** 同样的规格和 `seed`，在 `js`、`wasm-gc`、`native` 上输出逐字节相同。每个图形有自己的 seed：由基础 seed 和图形在场景图里的位置
（图元、标记、图元……的下标序列）做哈希得到，落在 `[1, 2^31-2]`。所以柱子不会抖得一模一样，增删一个数据点也不会改变别的图形。
输出用官方包验证过：16 个用例（柱、分层、各种形状的符号、面积、弧形、MoonFrame、其他填充样式、粗糙度、弯曲度、斜线间距和线宽、取整）的 SVG 与
vega-lite 5.23.0 + vega 5.33.1 + rough.js 4.6.6 给出的逐字节相同（`scripts/lib/sketch.mjs`、`fixtures/sketchcases`）；
[画廊](gallery/index.html)里的 32 张图也都逐字节相同，每张图有三个版本。
[在线演示](https://sayoi7799.github.io/moon_vegalite/)里有「手绘风格」开关，可以改 seed 和粗糙度。

## 在线演示

**<https://sayoi7799.github.io/moon_vegalite/>**：修改 Vega-Lite 规格，实时看到本库（编译成 JavaScript，运行在你的浏览器里）渲染的 SVG，
旁边是官方 vega-embed 的渲染结果，并有标签说明两边的 SVG 文本是否逐字相同。示例包括官方图表、分层图、
MoonFrame 生成的规格，以及几个不支持的特性（会显示带 JSON 路径的错误）。页面由 `.github/workflows/pages.yml`
用 [`site/`](site) 和 `web` 包（`moon build --target js --release`）构建；本地运行：

```sh
moon build --target js --release
node scripts/build_site.mjs && node scripts/serve_site.mjs     # http://localhost:8080/
```

## 用 MoonFrame 从 CSV 生成图表

[`examples/moonframe_demo`](examples/moonframe_demo) 用 [MoonFrame](https://github.com/ihb2032/MoonFrame) 读取 CSV 并做汇总，
由 MoonFrame 生成 Vega-Lite 规格，再用本库直接渲染成 SVG，整个过程是一个 MoonBit 程序，不需要打开 Vega 编辑器。
它写出的三张 SVG 与官方 vega 的输出逐字相同。MoonFrame 0.7.0 在当前工具链下需要一步准备工作，
由示例里的 `setup.sh` 完成，示例自己的 README 里有说明。

## 把报错反馈给大模型

大模型常常直接输出 Vega-Lite，但经常写错或用到不支持的特性；本库的报错以出错属性的 JSON 路径开头。
[`examples/llm_feedback`](examples/llm_feedback) 用事先录好的大模型回复作为输入（不需要 API key，不访问网络）：
能渲染的输出 SVG，渲染失败的输出一条包含 JSON 路径和原因的修正提示，可以直接发回给模型。

## 与官方输出并排对照

左：本库；中：vega-lite 5.23.0 + vega 5.33.1，除无障碍属性外 SVG 文本逐字相同；右：手绘风格（见下）。
直接用浏览器打开 [`gallery/index.html`](gallery/index.html)（不需要启动服务器）可以看到全部 32 张图，每张都有三个版本。

![画廊页面](docs/gallery.png)

| 图 | moon_vegalite | 官方 vega | 手绘风格 |
| --- | --- | --- | --- |
| `stacked_bar_v` | <img src="gallery/ours/stacked_bar_v.svg" width="230" alt="moon_vegalite: stacked_bar_v"> | <img src="gallery/official/stacked_bar_v.svg" width="230" alt="vega: stacked_bar_v"> | <img src="gallery/sketch/stacked_bar_v.svg" width="230" alt="hand-drawn: stacked_bar_v"> |
| `point_color` | <img src="gallery/ours/point_color.svg" width="230" alt="moon_vegalite: point_color"> | <img src="gallery/official/point_color.svg" width="230" alt="vega: point_color"> | <img src="gallery/sketch/point_color.svg" width="230" alt="hand-drawn: point_color"> |
| `layer_bar_line` | <img src="gallery/ours/layer_bar_line.svg" width="230" alt="moon_vegalite: layer_bar_line"> | <img src="gallery/official/layer_bar_line.svg" width="230" alt="vega: layer_bar_line"> | <img src="gallery/sketch/layer_bar_line.svg" width="230" alt="hand-drawn: layer_bar_line"> |
| `layer_histogram` | <img src="gallery/ours/layer_histogram.svg" width="230" alt="moon_vegalite: layer_histogram"> | <img src="gallery/official/layer_histogram.svg" width="230" alt="vega: layer_histogram"> | <img src="gallery/sketch/layer_histogram.svg" width="230" alt="hand-drawn: layer_histogram"> |
| `layer_color_legend_left` | <img src="gallery/ours/layer_color_legend_left.svg" width="230" alt="moon_vegalite: layer_color_legend_left"> | <img src="gallery/official/layer_color_legend_left.svg" width="230" alt="vega: layer_color_legend_left"> | <img src="gallery/sketch/layer_color_legend_left.svg" width="230" alt="hand-drawn: layer_color_legend_left"> |
| `layer_circle_independent_color` | <img src="gallery/ours/layer_circle_independent_color.svg" width="230" alt="moon_vegalite: layer_circle_independent_color"> | <img src="gallery/official/layer_circle_independent_color.svg" width="230" alt="vega: layer_circle_independent_color"> | <img src="gallery/sketch/layer_circle_independent_color.svg" width="230" alt="hand-drawn: layer_circle_independent_color"> |

## 定位，以及与其他图表库的区别

* **跟随 Vega-Lite，而不是另起一套语法。** 目标是画出与参考实现（vega-lite 5.23.0 + vega 5.33.1）相同的图，而不是相似的图：
  坐标轴刻度、标签格式、图例、布局和图元包围盒都移植自 vega-lite / vega，包括它们在 JavaScript 数字和文字度量上的行为。
* **场景图优先。** 和 Vega 一样，库先生成一棵带有包围盒的图元树；SVG 只是这棵树的一种渲染器，别的后端也可以消费它。
  多数 MoonBit 绘图代码直接画图，这里的中间表示是公开的。
* **规格进，图片出。** 图表就是数据（JSON），可以来自别的工具。测试会把
  [MoonFrame](https://github.com/ihb2032/MoonFrame)（`DataFrame` → Vega-Lite）生成的规格交给渲染器。
* **不需要浏览器，不需要 JavaScript 运行时。** 渲染是一个纯函数，可用于原生命令行、WebAssembly 或 JS。
* **如实说明范围。** 这是一个子集渲染器。子集之外的内容会报错并给出 JSON 路径
  （如 `encoding.color.condition 暂不支持`），而不是悄悄画成别的样子。

## 支持范围

下面是已经实现的内容。其余一律报错并给出 JSON 路径（`encoding.color.condition 暂不支持`、`mark.tooltip 暂不支持`）；
即使是已支持特性上的属性，没实现的也会报错，不会被悄悄忽略（`vl/validate.mbt`、`vl/errors_test.mbt`）。

| 方面 | 支持内容 |
| --- | --- |
| 数据 | 内嵌的 `data.values`。不读取 `data.url` 和 `datasets`，`data.format.parse` 会报错：请把数据直接写进规格 |
| 标记（mark） | `bar`、`line`、`point`、`circle`、`square`、`area`、`rule`、`tick`、`rect`、`text`、`arc` |
| mark 属性 | `color`、`filled`、`opacity`、`interpolate`（`linear`、`step`、`step-before`、`step-after`、`monotone`）；`rule` 和 `line` 的 `size`（线宽）；`rule`：`strokeDash`；`text`：`align`、`baseline`、`dx`、`dy`、`fontSize`、`text`、`x`、`y`（`"width"` / `"height"` 或像素）；`arc`：`innerRadius`、`outerRadius`、`padAngle`、`cornerRadius`、`stroke`、`strokeWidth` |
| 位置通道 | `x`、`y`（字段或数值 `datum`）、`x2`、`y2`（用于 `rule`、`bar`、`rect`：区间柱、区间矩形、横贯绘图区的线）、`theta`（用于 `arc`） |
| 其他通道 | `color`（名义字段用分类色板，有序字段按取值个数从 blues 色带取样，定量字段用 blues 渐变并配渐变图例，常量值）、`size`、`opacity`（也可以是常量）、`shape`（名义型）、`text`（可带 `format`）；没有字段的 `aggregate: count` 可用于 size、颜色、透明度和形状 |
| 字段类型 | `quantitative`、`ordinal`、`nominal`、`temporal` |
| 聚合 | `count`、`sum`、`mean`、`average`、`min`、`max`、`median`、`distinct` 等（vega 的聚合操作） |
| 变换 | `filter`（表达式或谓词）、`calculate`（表达式子集）、`aggregate`、`bin`、`timeUnit`、`fold`；字段上的 `bin`、`timeUnit`、`aggregate` |
| 时间单位 | year、quarter、month、week、date、day、hours、minutes、seconds、milliseconds 及其组合（按 UTC；"本地时间"即 UTC） |
| 比例尺 | 位置通道上的 `linear`、`log`、`sqrt`、`pow`、`time`、`utc`、`band`、`point`、`ordinal`，支持 `domain`、`domainMin`、`domainMax`、`range`、`zero`、`nice`、`reverse`、`clamp`、`base`、`exponent`；`arc` 的角度比例尺支持 `range` |
| 堆叠 | `bar` 和 `area` 默认堆叠（堆叠面积图会像 vega-lite 那样补零）；`stack: zero / center / normalize / null`；`arc` 把 `theta` 绕圆堆叠（`zero`、`normalize`） |
| 排序 | 离散 x / y 上的 `sort`：`ascending`、`descending`、取值列表、`-x` / `-y`、`{op, field, order}` |
| 坐标轴 | 网格、刻度、带首尾对齐和重叠剔除的标签、标题；属性 `domain`、`grid`、`labels`、`ticks`、`format`、`labelAngle`、`labelFlush`、`labelOverlap`、`tickMinStep`、`titlePadding`、`offset`、`position` 以及颜色 / 宽度 / 大小一类；`axis: null` |
| 图例 | 符号图例（大小、形状、透明度、颜色，字段相同时合并）、定量颜色的渐变图例（默认 blues 色带）；`legend.orient`：`left` 或 `right`；`legend: null` |
| 分层（layer） | `layer`，可嵌套，继承 `data` / `transform` / `encoding`；位置比例尺默认共享（合并定义域，band 优先于 point），也可用 `resolve.scale.x / y` 独立（各有一根轴，第二根放到另一侧）；颜色比例尺及其图例默认共享，也可用 `resolve.scale.color` 独立；一个 size 比例尺配多个独立图例（`resolve.legend.size`）；对同一字段做相同 `bin` 的各层共用一次分箱和一个分箱比例尺；坐标轴和图例标题按 vega-lite 的规则合并 |
| 弧形 | `mark: arc` 加 `theta` 和 `color`：饼图和环形图，画在 200 × 200 的无边框视图里 |
| 布局 | 单视图或 `layer`；`width` / `height` 取数字或 `{step}`；`padding`；`background`；字符串形式的 `title` |
| 手绘风格 | `render_svg_sketch` / `vl2svg --sketch`：矩形、线、面积、符号、弧形和绘图区边框交给 moon_rough 来画，文字不变（见上） |
| 配置 | 只有 `config.scale.zero`；其他 `config` 键一律带路径报错 |
| 接受但不影响画面 | `description`、`name`、`usermeta` |

不支持，每一项都会报错并给出路径：交互（`params`、`selection`、`tooltip`、`condition`）、`projection` 和地理标记、
`facet`、`concat`（`hconcat` / `vconcat`）、`repeat`、`autosize`、对象形式的 `title`、`radius` / `theta2` 通道和极坐标文字、`trail`、
`boxplot`、`errorbar`、`errorband`、变换 `window`、`joinaggregate`、`flatten`、`loess`、`regression`、`density`、`pivot`、`lookup`、
完整的 Vega 表达式语言（只实现了 `calculate` / `filter` 用到的子集）、比例尺类型 `symlog`、`quantile`、`quantize`、`threshold`、
`bin-ordinal`、`sequential`，非位置通道上的比例尺选项（`scheme`、`range`），`scale.zero` 以外的 `config`，以及上面没有列出的坐标轴和图例选项。

## 与官方实现的一致性

整体流程、为什么能和官方输出一致、测试是怎么做的，见 [`docs/defense.md`](docs/defense.md)。

测试里所有期望值都来自官方包，没有一个来自本仓库自己：

| 层次 | 参照 | 位置 |
| --- | --- | --- |
| 数字格式化、刻度、`nice`、时间格式、比例尺 | vega 5.33.1 内置的 d3 / vega-format / vega-scale | `fixtures/{numfmt,ticks,formatcases,timecases,scalecases}` |
| 表达式、数据变换 | vega-expression、vega-transforms | `fixtures/{exprcases,transformcases}` |
| 场景图包围盒、SVG | vega-scenegraph | `fixtures/scenecases` |
| **端到端：80 个官方 Vega-Lite 示例（其中 15 个是 `layer_*`，2 个是 `arc_*`）、边界情况、MoonFrame 规格** | vega-lite 5.23.0 编译 + vega 场景图与 SVG | `fixtures/vlexamples` |

端到端用例要求场景图（每个 mark、每个图元属性和包围盒，数值在相对 `1e-9` 内）和 SVG 文本
（逐字相同，只忽略无障碍属性 `role` / `aria-label`）都与官方输出一致。
用例包括 `examples/official.txt` 里列出的 80 个官方示例、`examples/edge_*.vl.json` 里的边界情况、
`examples/channel_*.vl.json` 里的通道规格（空数据、全空列、单值定义域、含非正值的对数比例尺），
以及 MoonFrame 生成的六张图（`examples/moonframe`）。非法 JSON 和不支持的特性由 `vl/errors_test.mbt` 覆盖。

`moon test` 在 `wasm-gc`、`js`、`native` 上均通过。

### 测试数据的来源与重新生成

测试数据是 `scripts/gen_fixtures.mjs` 生成的 MoonBit 文件，使用 `scripts/package-lock.json` 里锁定的版本
（vega-lite 5.23.0、vega 5.33.1、vega-datasets 3.2.1）和 Node.js 24（部分期望值依赖 V8 的 `Math` 函数，所以 Node 主版本号很重要）。
进程时区固定为 UTC。

```sh
cd scripts
npm ci
npm run fixtures            # 下载 vega-lite 对应标签的示例规格，重新生成 ../fixtures
npm run gallery             # 重新生成 ../gallery（需要先 moon build --target native cmd/vl2svg）
```

CI 会重新生成测试数据，只要已提交的生成文件有任何不同就会失败。

`util/pow10_table.mbt` 保存整数 k 在 [-330, 310] 内的 10^k。它按 `Number("1e" + k)`（正确舍入的值）生成，
而不是 `Math.pow(10, k)`：V8 的 `Math.pow` 并不一致，在 Linux、macOS、Windows 版 Node 24 之间有少数指数相差一个 ulp
（Node 22 和 24 之间约有 70 个指数不同）。因此 `js_pow10` 是在大多数构建上与 V8 一致，而不是只与某一个特定构建一致。
与生成旧表的 Windows 构建相比，只有 10^23 变了，包括与官方输出端到端对照在内的完整测试保持不变。

`examples/moonframe/*.vl.json` 由 `ihb2032/MoonFrame` 0.7.0（`@chart.format_vega_lite`）生成。MoonFrame 0.7.0 目前
无法直接用最新的 MoonBit 工具链构建（它的依赖 `NyaCSV` 和 `quickcheck` 仍带着旧的 `moon.mod.json`，
`NyaCSV` 里还有三个测试用到了已被移除的 `Buffer` API），所以这些规格是在一个临时工程里，
迁移了这些清单文件并在本地删掉那三个测试后生成的。生成出的 JSON 原样提交。

## 性能

两个场景，每一列用的是同一份规格（由 `benchmarks/data.mbt` 生成；`scripts/bench/specs.mjs` 用 JavaScript 实现了同一个生成器，测试里固定了用 JavaScript 算出的校验和）。
每次运行都从规格文本开始，和 `render_svg` 一样。「到 SVG」是完整流程；「到场景图」是在写出 SVG 文本之前停下。数值是 10 批的平均值 ± 标准差。

| 场景 | native | js | wasm-gc | Node 24 + vega-lite 5.23.0 + vega 5.33.1 |
| --- | --- | --- | --- | --- |
| 1 万行数据按类别求均值画柱状图：到 SVG | 21.5 ± 0.4 ms | 9.5 ± 0.2 ms | 8.6 ± 0.7 ms | 5.9 ± 0.8 ms |
| 1 万行数据按类别求均值画柱状图：到场景图 | 21.4 ± 0.6 ms | 9.6 ± 0.4 ms | 7.9 ± 0.2 ms | 4.5 ± 0.3 ms |
| 10 万个点的散点图：到 SVG | 1.68 ± 0.03 s | 0.83 ± 0.03 s | 0.68 ± 0.04 s | 0.88 ± 0.04 s |
| 10 万个点的散点图：到场景图 | 0.66 ± 0.02 s | 0.32 ± 0.03 s | 0.28 ± 0.03 s | 0.17 ± 0.01 s |

两个场景输出的 SVG 都与官方逐字相同（散点图的 SVG 有 15.8 MB），由 `scripts/bench/vega_bench.mjs --check` 检查。

```sh
moon bench --release --target js -p sayoi7799/moon_vegalite/benchmarks        # 另外还有 wasm-gc 和 native
moon build --target native cmd/vl2svg                                          # 检查 SVG 是否相同时要用到这个可执行文件
cd scripts && npm ci && npm run bench                                          # vega 一侧，同时检查 SVG 是否相同
```

数字说明的事：

* 小图上 Node 里的 vega 更快（5.9 ms，对比 `wasm-gc` 和 `js` 的 8.6 到 9.5 ms）：JIT 已经热了，也没有启动开销。
* 10 万点散点图上，`wasm-gc`（0.68 s）和 `js`（0.83 s）端到端和 Node 里的 vega（0.88 s）一样快。构建场景图比 vega 慢
  （0.28 到 0.32 s，对比 0.17 s），写出 SVG 则更快（0.40 和 0.52 s，对比 0.71 s）。
* `native` 最慢，在这种大量分配内存的工作上大约是 `wasm-gc` 和 `js` 的两倍。这里只做了测量，没有调查原因，也没有优化。

测量环境：一台机器（AMD Ryzen 9 7945HX，Windows 11），每种配置跑一次，`moon 0.1.20261016` release 模式，Node 24.20.0。
vega 一侧在三次预热后计时，并且包含对规格文本的 `JSON.parse`。这些数字只是参考，不是结论。

## 目录结构

| 包 | 内容 |
| --- | --- |
| `util`、`stats`、`format`、`time`、`value` | 与 JavaScript 一致的数字运算（V8 libm）、d3 刻度、d3-format、时间间隔与格式、动态值 |
| `scale` | 连续、band、point、ordinal、sequential 比例尺，色板 |
| `expr`、`transform` | 表达式子集；aggregate / bin / stack / timeunit / fold / filter / formula |
| `scene`、`svg` | 场景图、mark 包围盒、路径生成、SVG 渲染器 |
| `spec` | Vega-Lite JSON 的严格读取器（不支持的特性带路径报错） |
| `vl` | 编译器：数据管线、比例尺、坐标轴、图例、布局 |
| `sketch` | 手绘风格：场景图里的图形交给 moon_rough 来画 |
| `web` | 演示页的入口（js）：`render`、`render_sketch` |
| `benchmarks` | 性能表用的 `moon bench`（不属于库） |
| `cmd/vl2svg` | 命令行工具（native） |

## 开发记录说明

2026 年 10 月 24 日之前的大部分工作是在本地完成的，没有逐步提交。10 月 24 日把已有代码按模块整理成提交
（底层数字与时间库、比例尺、表达式与变换、场景图与 SVG、规格读取器、编译器各阶段、测试、CI）。
当时 GitHub 上的历史被改写过一次，用来把几个后续的 CI 修复合并进一个提交。此后每一项改动都在
各自的分支上、针对一个 GitHub issue 完成，并通过关闭该 issue 的 pull request 合并。

## 许可证

Apache-2.0。部分代码移植自 vega、vega-lite 和 d3（BSD-3-Clause / ISC）以及 V8 采用的 fdlibm，它们的版权声明和许可证全文见 [`THIRD_PARTY_LICENSES`](THIRD_PARTY_LICENSES) 和各文件头部。
手绘风格依赖 [moon_rough](https://github.com/sayoi7799/moon_rough)（MIT），它是 [rough.js](https://github.com/rough-stuff/rough)（MIT）的移植。
