文章
模型驱动视图:LLM 生成可视化代码的路径与挑战
从 D3 到 ECharts,LLM 生成可视化代码的几种路径、Prompt 工程策略,以及 Scott Logic 和 Observable 的实战经验。
让模型”画图”有两种主流路径:多模态直接生成图像,或者让模型写代码来渲染。
前者的上限受限于图像模型对”准确性”的理解——柱状图的高度、饼图的比例,很难保证精确。后者的本质是让模型写代码,渲染交给专业库,准确性由代码逻辑保证。
这条路走得通吗?能走多远?
代码生成的三种模式
模型生成可视化代码,大致有三种模式:
1. 直接生成库代码(D3/ECharts/Chart.js)
这是最直接的方式:告诉模型你要什么图,它输出 JavaScript/Python 代码。
// 模型输出的 D3 代码
const svg = d3.select("body").append("svg")
.attr("width", 960).attr("height", 500);
svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", d => x(d.category))
.attr("y", d => y(d.value))
.attr("width", x.bandwidth())
.attr("height", d => height - y(d.value));
优点:灵活性最高,理论上能生成任何图表。 问题:代码量大(D3 一张基础图表 200-300 行),语法错误率高,版本混乱(D3 v4 vs v7 API 差异大)。
2. 生成配置对象(ECharts/Vega-Lite)
这种方式让模型输出 JSON 配置,而不是命令式代码。
// 模型输出的 ECharts 配置
{
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 20, 30] }]
}
优点:结构化输出,语法错误少,模型更容易”填空”。 问题:受限于库的能力边界,高度定制化的需求难以满足。
3. 生成领域特定语言(DSL)
AntV 的 GPT-Vis 和 Infographic 走的是这条路:设计一种专门给 LLM 用的语法。
vis line
data
- time 2020
value 100
- time 2021
value 120
style
lineWidth 3
优点:语法极简,模型生成准确率高,支持流式渲染。 问题:需要额外开发 DSL 解析器和渲染引擎,能力受 DSL 设计限制。
实战:LLM 写 D3 代码到底靠不靠谱
Scott Logic 团队做过一组系统性的实验:用不同 LLM 生成 D3 代码,看成功率。
简单 Prompt 的表现
直接问:
写一段 D3 代码,展示 2022 赛季 F1 每位车手的获胜场次。
结果:
- GPT-4:50% 成功,其余有坐标轴裁切、元素不存在等问题
- GPT-3.5-turbo:30% 成功
- text-davinci-003:60% 成功,但输出不稳定
典型失败模式:
- 版本混淆:调用了 D3 v4 的
d3.nest(),但页面加载的是 v7 - 容器不存在:
d3.select("#chart"),但 HTML 里没有这个元素 - 边距问题:坐标轴渲染在 SVG 可视区域之外
- 假数据:模型自己编了一组数据,而不是加载真实数据
改进:提供数据结构
把 Prompt 改成:
数据在
data-sources/2022_f1.csv,字段包括 Circuit、Date、1st Place Driver…
成功率明显提升。模型能理解”1st Place Driver 字段出现次数 = 获胜场次”,并生成正确的聚合逻辑。
这说明:模型对数据语义的理解比预期强,但需要你明确告诉它数据长什么样。
再改进:One-shot Prompting
在 Prompt 里塞一个完整示例:
### INSTRUCTION
写 D3 代码展示每个班级的学生人数
### DATA
CSV 路径:data-sources/classes.csv
字段:Class, Pupils
示例数据:Biology,23 / History,28 / Latin,5
### RESPONSE
d3.csv('data-sources/classes.csv').then((data) => {
// 完整的 D3 代码...
// 包含正确的 margin、坐标轴、标签
});
### INSTRUCTION
写 D3 代码展示 2022 赛季每位 F1 车手的获胜场次
### DATA
CSV 路径:data-sources/2022_f1.csv
字段:Circuit, Date, 1st Place Driver...
### RESPONSE
使用 One-shot Prompting 后:
- GPT-4:80% 成功率
- GPT-3.5-turbo:40% 成功率
- text-davinci-003:50% 成功率
示例代码里的 margin 设置、坐标轴标签、元素选择方式,都被模型”学会”了。
关键挑战
版本碎片化
D3 的 v3、v4、v5、v6、v7 API 都有差异。训练数据里混杂着不同版本的代码,模型经常”缝合”出跨版本的代码。
Scott Logic 的实验里,有输出同时调用了 d3.nest()(v5)和 d3.group()(v6+),在任何版本都无法运行。
执行上下文缺失
模型不知道你的页面长什么样:
- SVG 容器存在吗?
- 用
<svg>还是<div>? - 需要响应式吗?
这些信息如果不写在 Prompt 里,模型只能猜。
数据变换逻辑
“展示每位车手的获胜场次”——这句话对人类很简单,对模型意味着:
- 遍历所有记录
- 按
1st Place Driver分组 - 计算每组的 count
- 按计数排序
模型能理解这个逻辑,但如果数据结构复杂(嵌套、多表关联),错误率会显著上升。
Observable 的发现
Observable 的 Robert Kosara 用 Claude Sonnet 3.7 做了类似实验。他的观察:
- 基础图表很稳:折线图、柱状图、散点图,一次性成功率很高
- 代码质量不错:结构清晰、注释完整,像人类写的
- 有些”奇怪”的实现:比如用一串 if-else 计算季度,而不是
Math.floor(month/3) + 1 - 对数据量的假设:给 12 行数据,模型会假设是完整数据集,加上数据点标记;给完整数据后,这些标记就显得冗余
Prompt 工程策略
基于这些实战经验,一套有效的 Prompt 结构:
### 任务描述
你要生成一段 [D3/ECharts/Chart.js] 代码,在 [浏览器/Node.js] 环境运行。
### 数据
- 来源:[CSV 路径 / 内嵌 JSON / API 端点]
- 字段:[列出所有字段及含义]
- 示例:[3-5 行真实数据]
### 执行环境
- 容器:页面已有 `<svg id="chart">` 元素
- 库版本:D3 v7.9.0
- 尺寸:800×600
### 图表要求
- 类型:[折线图/柱状图/...]
- X 轴:[字段名],标签为"[中文标签]"
- Y 轴:[字段名],标签为"[中文标签]"
- 交互:[tooltip/zoom/...]
### 代码注释
在数据变换的关键步骤添加注释,说明你在做什么。
核心原则:
- 明确执行环境:容器、版本、尺寸
- 提供数据结构:字段名 + 含义 + 示例数据
- 要求代码注释:强制模型”思考”数据变换逻辑
- 用示例引导:One-shot 比零样本强很多
DSL vs 原生代码
AntV 的 GPT-Vis 选择设计 DSL,而不是让模型直接写 JavaScript。这个选择的逻辑:
| 维度 | 原生代码 | DSL |
|---|---|---|
| 语法复杂度 | 高(引号、括号、分号) | 低(缩进 + 关键词) |
| 错误类型 | 语法错误 + 运行时错误 | 主要是语义错误 |
| 流式渲染 | 困难(代码不完整时无法解析) | 容易(逐行解析) |
| 灵活性 | 最高 | 受 DSL 设计限制 |
| 模型准确率 | 60-80% | 90%+(根据 AntV 公布的数据) |
DSL 的本质是把”模型的输出空间”压缩到一个更小、更可控的范围。
小结
让模型生成可视化代码这条路是走得通的,但有几个前提:
- Prompt 要足够具体:执行环境、数据结构、示例代码,缺一不可
- 选对目标语言:ECharts 配置比 D3 代码更容易生成;DSL 又比配置更容易
- 接受 20% 的失败率:即使是最强的 GPT-4,one-shot 模式下也只有 80% 成功率
- 人工兜底:生成后需要 review,尤其是数据变换逻辑
更深层的问题是:代码生成本质上是把”理解需求”的负担转嫁给模型。如果需求本身模糊(“做一个好看的销售图表”),模型只能猜。如果需求精确(“X 轴是月份,Y 轴是销售额,用柱状图”),模型的表现会很稳定。
这不是模型的问题,是需求表达的问题。好的可视化工具,应该让精确表达需求的成本足够低。
讨论
留下你的想法
欢迎补充观点、指出问题,或分享与你类似的实践经验。
💬 留言评论
欢迎交流讨论,提问或分享你的想法。