AntV 做了十几年图表,为什么还要再写一个信息图引擎

发布于 · 2,727 字 · 约 7 分钟#Github 解读#AI 生图原文链接
AntV 做了十几年图表,为什么还要再写一个信息图引擎 封面图
  • 设计面向 AI 的 DSL 语法,缩进+键值对,AI 按模式填空即可生成
  • 流式渲染支持 AI 逐 token 输出时实时渲染,解析器容错不崩溃
  • 内置关系表达式和 200+模板,模板名支持模糊匹配,降低 AI 生成门槛
  • 提供完整编辑器系统,支持 AI 生成后人工微调,实现 AI+人工协作

六年前我在一家数据公司写前端,接到一个需求:把季度运营报告做成一张信息图,要让老板一眼看完。

当时我打开 Sketch,画了三个小时。画完我就在想,这活儿要是能让 AI 干就好了。

六年后的今天,AI 确实能干了。但一个新的问题冒出来了:AI 画出来的东西,怎么让它不丑?

你给 GPT-4o 说「帮我画一个时间线」,它给你一段 HTML+CSS,渲染出来要么布局错位,要么颜色辣眼。你给 Claude 说「画一个 SWOT 分析图」,它给你一段 Mermaid 代码,渲染出来就是个方框加几行字,谈不上「信息图」。

AntV 团队大概也看到了这个 gap。2025 年 9 月,他们发布了一个叫 Infographic 的项目,到现在 11 个月,攒了 5600 多星。

说真的,我第一反应是:AntV 已经有了 G2(统计图表)、G6(图分析)、Graphin(图可视化应用)、S2(多维交叉分析表),为什么还要再写一个信息图引擎?

读完源码我才明白,这不是又一个图表库。这是一套从底层语法到渲染引擎到编辑器,全部为 AI 生成而设计的系统。

图表库和 AI 之间,有一道语法鸿沟

先聊聊为什么 G2 不够用。

G2 的语法是声明式的,没错。但它的声明式面向的是「开发者写配置」,不是「AI 写配置」。举个例子,G2 画一个柱状图,你得写几十行配置:

chart.interval().position('genre*sold').color('genre')

这段代码本身没问题。问题在于,当 AI 生成时,它需要理解 interval 是什么、position 是什么、数据格式是什么。如果 AI 的上下文窗口不够大,或者训练数据里 G2 的样本不够多,生成的配置基本就是错的。

Infographic 的做法是,从头设计一个 DSL。

这个 DSL 长这样:

infographic list-row-simple-horizontal-arrow
data
  lists
    - label Step 1
      desc Start
    - label Step 2
      desc In Progress
    - label Step 3
      desc Complete

看到区别了吗?没有嵌套的 JSON 对象,没有花括号,没有逗号。就是缩进 + 键值对。AI 生成这种格式,几乎不需要「理解」什么,照着模式填空就行。

打开 src/syntax/parser.ts,这个缩进解析器写得很有意思。它用了一个栈帧来追踪缩进层级:

interface StackFrame {
  indent: number;
  node: ObjectNode | ArrayNode;
  parent?: ObjectNode | ArrayNode | null;
  key?: string | null;
}

每一行解析时,先算缩进层级,然后 while (stack.length > 1 && indent <= stack[stack.length - 1].indent) { stack.pop() },回到正确的父节点。这个模式跟 Python 的缩进解析、Starlark 的解析器思路是一样的。

但有一个细节值得说:它还支持点号路径。assignObjectEntry 函数里,如果 key 包含 .,它会按路径逐级创建对象。比如 a.b.c value 会生成 {a: {b: {c: value}}}。这个设计对 AI 很友好,因为 AI 有时候会「偷懒」,想在一行里表达嵌套结构。

流式渲染,是 AI 生成内容的关键技术

AI 生成内容是一个 token 一个 token 吐出来的。如果每次都要等完整输出才渲染,用户等不了。

Infographic 的流式渲染能力,是它跟传统图表库最大的区别。

用法很简单:

let buffer = '';
for (const chunk of chunks) {
  buffer += chunk;
  infographic.render(buffer);
}

每收到一个 chunk,就调一次 render。问题在于,AI 输出到一半的时候,语法是不完整的。比如只输出了 data 还没输出数据,或者只输出了模板名还没输出配置。这时候如果解析器报错,整个渲染就断了。

Infographic 的解法在 src/syntax/mapper.ts 里。它的 mapWithSchema 函数对每个字段都做独立验证,遇到不完整的字段不会抛异常,而是返回 undefined 并记录一条错误。渲染器看到 undefined 就跳过,不阻塞流程。

src/runtime/Infographic.tsx 的 render 方法也做了同样的容错:

private performRender() {
  const parsedOptions = this.parsedOptions;
  if (!isCompleteParsedInfographicOptions(parsedOptions)) {
    this.emitter.emit('error', new Error('Incomplete options'));
    return;
  }
  // ...render
}

如果选项不完整,它 emit 一个 error 事件,但不会崩溃。下一次 render 调用会继续。这意味着 AI 可以一边吐一边渲染,每次渲染都是「当前最佳状态」。

坦白讲,这个设计思路跟不少 AI 流式渲染方案很像,但 Infographic 做得更彻底——它的 DSL 解析器本身就有容错能力,不是靠外层 try-catch 兜底。

一行关系表达式,背后是完整的图解析

Infographic 的 DSL 里有一个让我眼前一亮的特性:关系表达式。

你可以直接在 data 的 relations 字段里写类似这样的语法:

A -> B[label: depends on] -> C

或者更复杂的双向箭头:

A <-> B[label: peer]
A - B[label: connects]

这段解析逻辑在 src/syntax/relations.ts 里。parseRelationLine 函数用了一段手写的状态机来解析箭头表达式。它支持:

  • -> 单向箭头
  • <-> 双向箭头
  • - 无向连接
  • A -- B 长连接
  • 节点标签用 [label] 或 (label) 标注
  • 管道符 | 分隔的双向标签

readEdge 函数里有一段处理双向箭头的代码很有意思:

// Detect split bidirectional arrow pattern: <- label ->
{
  const leftHasLeft = directionToken.includes('<');
  const leftHasRight = directionToken.includes('>');
  if (leftHasLeft && !leftHasRight) {
    // look ahead for a matching right arrow
  }
}

这个模式匹配的是 A <- label -> B 这种写法。左边有一个向左的箭头,右边有一个向右的箭头,中间是标签。这种写法在 ASCII 图里很常见,但在 DSL 解析器里实现起来还挺麻烦的。

模板系统:200 个模板,每个都是精心编排

Infographic 内置了大约 200 个模板。这些模板不是简单的「名字 + 配置」映射,每一层都有设计。

看 src/templates/built-in.ts,每个模板的定义都是一个 TemplateOptions 对象,包含 design(结构 + 组件 + 数据项)和 themeConfig(主题配置)。比如 list-row-simple-horizontal-arrow 的定义:

'list-row-simple-horizontal-arrow': {
  design: {
    title: 'default',
    structure: { type: 'list-row', gap: 0, zigzag: true },
    items: [{ type: 'simple-horizontal-arrow' }],
  },
},

模板名本身就是一个「三层命名」:布局类型-子类型-组件名。list-row 是布局,simple-horizontal-arrow 是数据项组件。这种命名方式让模板名本身就有了可读性。

模板注册系统在 src/templates/registry.ts 里,用了一个 Map<string, TemplateOptions>。同时有一个 resolveTemplateKey 函数,它会做模糊匹配——如果用户输入的模板名不完全匹配,它会用 findClosestTemplateKey 找最接近的。这个功能对 AI 很友好,因为 AI 经常记不住完整模板名。

编辑器:AI 生成之后,人可以微调

AI 生成的信息图,不可能一次就完美。用户需要手动调整。

Infographic 内置了一个完整的编辑器系统,在 src/editor/ 目录下,代码量不小。它包含:

  • 命令系统:src/editor/commands/ 下的 UpdateElement、UpdateText、UpdateOptions、Batch,支持撤销/重做
  • 交互系统:src/editor/interactions/ 下的 ClickSelect、DragElement、DragCanvas、ZoomWheel、BrushSelect、DblClickEditText
  • 插件系统:src/editor/plugins/ 下的 EditBar、ResizeElement、ResetViewBox、CoreSync
  • 状态管理:src/editor/managers/ 下的 CommandManager、InteractionManager、PluginManager、StateManager、SyncRegistry

这个编辑器不是摆设。EditBar 插件提供了完整的编辑工具栏,支持字体、颜色、对齐等操作。CoreSync 插件负责同步编辑状态和渲染状态。

AI 生成 + 人工微调,是目前最务实的工作流。纯 AI 生成的信息图,在可预见的未来里,都还需要人来把关。

诚实的边界

项目还在 0.x 版本。这本身就是一个重要的「限」。我翻了 issue 区,有几个问题值得关注。

第一,很多模板的 value 字段支持不完整。

Issue #211 里有人反馈:Skill.md 的示例里写了 value 字段,但很多模板渲染时根本不显示这个字段。我看了 src/syntax/schema.ts 里的 itemDatumSchema,value 确实是 union(number(), string()),属于 schema 支持但具体模板实现没跟上的情况。对于想用 value 显示数值的用户来说,这个坑不小。

第二,图表类模板的维度支持有限。

Issue #140 说,折线图等图表只支持一维数据。对比 ECharts 的 dataset 多维度支持,差距明显。在 src/designs/structures/chart-line.tsx 里,确实只处理了单条线的情况。

第三,缺少交互能力。

Issue #143 问怎么开启 hover 交互效果。目前的信息图是静态的,没有 tooltip 或 hover 高亮。对于习惯了 ECharts 交互的用户来说,这算是一个功能缺失。

第四,项目不到一年,版本迭代快。

从 2025 年 9 月到现在,发布了 20 个版本(0.2.14 到 0.2.19),几乎每两周一个版本。活跃度是好事,但也意味着 API 还在快速变化。如果你现在写一套基于 Infographic 的系统,半年后可能需要大改。

第五,包体积不算小。

package.json 里 size-limit 配置的目标是 500KB(gzip)。对比 G2 的 ~300KB,D3 的 ~250KB,这个体积在中大型项目中可以接受,但在移动端或首屏加载场景下,需要考虑按需加载。

隐式与显式之间的设计平衡

读源码的过程中,我发现了一个微妙的设计决策。

在 src/syntax/index.ts 的 parseSyntax 函数里,有一个 inferTemplateFromBareFirstLine 函数。它的作用是:如果用户没有显式写 infographic 或 template 关键字,解析器会尝试把第一行当作模板名来推断。

比如用户写了:

list-row-simple-horizontal-arrow
data
  lists
    - label Step 1

解析器会识别出 list-row-simple-horizontal-arrow 不是已知的根级关键字,把它当作模板名处理,并 emit 一条 warning:

Inferred template from a bare first line. Prefix it with "infographic" or "template" to make the syntax explicit.

这个设计很聪明。对 AI 来说,少写一个关键字就少一分出错的可能。但对人类开发者来说,隐式推断可能让人困惑——为什么我写了一个不存在的关键字,它不报错反而正常运行?

这就是「隐式与显式」的经典权衡。Infographic 选择了 AI 优先,用 warning 而非 error 来处理。这个取舍,值得每个做 AI 工具的开发者思考。

给 AI 配一门 DSL,比配一个 API 有用

写到最后,我想提炼一个模式。

过去两年,AI 工具的主流做法是给 AI 配 API 调用能力。让 AI 调 G2 的 API 画图,调 ECharts 的 API 画图,调 Canva 的 API 做设计。但 API 调用的问题是:AI 需要用对函数名、传对参数、处理好返回值。任何一个环节出错,输出就废了。

Infographic 代表的思路是:给 AI 配一门 DSL,让 AI 用 DSL 描述意图,再由引擎渲染。

DSL 比 API 更接近 AI 的生成模式。AI 本质上是语言模型,它的强项是生成语言,不是调用函数。一门好的 DSL,应该让 AI 的「语言生成」能力直接映射到「内容生成」结果。

这个模式不只适用于信息图。Mermaid 用 DSL 让 AI 画流程图,Tailwind 用 CSS class 让 AI 做样式,SQL 用声明式查询让 AI 做数据分析。它们的共同点是:DSL 降低了 AI 的「行为空间」,让 AI 不需要理解复杂的 API 调用链,只需要在 DSL 的语法框架内填空。

Infographic 把 DSL 的容错性做到了流式级别,这是它跟 Mermaid 这些前辈最大的区别。AI 输出到一半,渲染器已经开始工作了。

一点想法

AntV 做了 G2、G6、S2、L7,十几年的积累。但面对 AI 时代,他们没有选择「改造 G2 让它支持 AI」,而是从零写了一个新引擎。

这个决策本身就有意思。改造一个成熟的库,看似省力,但架构约束会让你寸步难行。G2 的语法是面向开发者的,它的设计假设是「写配置的人能看懂文档」。Infographic 的语法是面向 AI 的,它的设计假设是「写配置的人可能根本不看文档,直接给个例子它就照着写」。

两个假设,两条路。

Infographic 选择了更难的那条路,但方向是对的。

评论互动

© 2026 王若风的技术博客 · Powered by Astro