开源项目 OfficeCLI 的图表体系:两条线、七套预设与一个渲染器
本文基于 OfficeCLI 仓库 commit 459b1a4(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/iOfficeAI/OfficeCLI 最新代码与文档为准。
让 Agent 画一张图表,难点不在”画”,而在你得先知道自己要走的是哪条代码路径——这个仓库里的图表根本不是一个模块,而是两条互不相通的构建线,外加一套预设和一个只为让 Agent 自己看结果而存在的渲染器。 选错了线,报错信息会告诉你某个类型”支持”,但真正执行时它压根不走你以为的那段代码。
先说清楚这里说的 OfficeCLI 是什么:它是托管在 GitHub 上的一个开源项目(仓库 https://github.com/iOfficeAI/OfficeCLI,Apache-2.0 许可,NOTICE 文件写明 Copyright 2026 OfficeCLI,由 goworm 创建维护),提供一个不依赖本机安装 Office 就能读写 .docx / .xlsx / .pptx 的命令行二进制。它不是微软的产品,也不是”用命令行操作 Office 应用”这类泛指的说法——下文提到 Word / Excel / PowerPoint 时,指的是文件格式和真实的应用程序,不是这个项目的归属。
站内已有几篇相邻的文章:让 AI 写数据分析脚本 讲的是数据本身怎么算,结构化输出不稳怎么办 讲的是模型吐出来的字段怎么才靠得住,AI 建站工具横评 讲的是产出网页那条路。这篇只管一件事:数据已经算好了、字段也已经稳了,把它落成一个真实 Office 文件里的图表,中间还隔着哪几层。
一、两条线是怎么分岔的
打开 src/officecli/Core/Chart/,目录下 14 个 .cs 文件,两万两千多行代码(wc -l 数得出来,你自己也能复现)。按名字分成两族:ChartHelper.* 一族七个文件(含 ChartHelper.cs 本身),ChartEx* 一族四个文件(构建、属性写回、陪嫁资源、样式),剩下三个是 ChartPresets.cs 和拆成两半的 SVG 渲染器。
分岔点在类型名。ChartHelper.cs 里的 ParseChartType 是老图表那条线的入口,它把用户传进来的字符串规范化以后做 switch,认得 bar、column、line、pie、pieofpie、barofpie、doughnut、area、scatter、bubble、radar、stock、combo、waterfall 这些 kind,同时把 3d、stacked、percentStacked 当修饰词剥离出来单独返回。
ChartExBuilder.cs 里的 IsExtendedChartType 是新式图表那条线的入口,它比对的是一个只有六个成员的集合:
internal static readonly HashSet<string> ExtendedChartTypes = new(StringComparer.OrdinalIgnoreCase)
{
"funnel", "treemap", "sunburst", "boxwhisker", "histogram", "pareto"
};
这里有个第一次读代码必然会绊一下的地方:ParseChartType 那个”未知类型”的异常文案里,把 funnel、treemap、sunburst、boxWhisker、histogram、pareto 也一并列进了”Supported types”。它们确实被支持,但不是被这个函数支持——调用方在更早的位置就用 IsExtendedChartType 把这六种分流走了,能走到 ParseChartType 的 switch 还落到 default 分支,说明你输入的是真的错别字。报错文案在这里承担的是”给人指路”的职责,不是”这段代码能处理”的声明。写 Agent 工具描述的人应该注意这个分寸:报错文案里的”支持列表”和函数实际处理范围,是两件事。
第二个分岔是瀑布图。ChartExBuilder.cs 的类注释里把 waterfall 列进了”Office 2016 扩展图表类型”,还标了”(native)“,但 ExtendedChartTypes 集合里没有它。真实路径在 ChartHelper.Builder.cs 的 case "waterfall",注释一句话说破:用堆积条形模拟。ChartHelper.Advanced.cs 里做具体拆解——把每个点拆成 base、increase、decrease 三组值堆在一起。base 那组是托底用的,代码显式给它设了无填充无描边,所以你看不见它;真正上色的是 increase(默认 4472C4 蓝)和 decrease(默认 FF0000 红),合计柱另有一个默认色 2E75B6 深蓝。最后一根柱默认当合计柱,从 0 起画累计值,你给的最后一个数值会被直接忽略,除非显式传 waterfallTotal=false。这不是 bug,是 OOXML 老图表规格里没有瀑布图这个原生结构时的常规做法,但 Agent 如果照着”我传了 5 个数就该画 5 个真实值”的假设去校验产出,会在最后一根柱上永远对不上。
二、老图表这条线:文本参数怎么变成 XML
OOXML 是 Office 文件的底层格式:一个 .xlsx 本质是个 zip 包,里面按目录放着一堆 XML,图表自己占一份 XML,用 c: 前缀的命名空间描述。老图表这条线要做的事,就是把命令行传进来的一堆 key=value 文本,翻译成这棵 XML 树。
翻译的第一关是 ParseSeriesData。它同时认三种写法:紧凑的 data=Sales:10,20,30;Cost:5,8,12,点号形式的 series1.name= / series1.values= / series1.categories=,以及旧式的 series1=Sales:10,20,30。还有个 series1Name= 的扁平别名,会被 NormalizeFlatSeriesNameAliases 改写成点号形式再往下走。这种”一个概念多种写法”的宽容,对 Agent 是好事——模型记不住唯一正确拼法时不至于整条指令失败;代价是解析函数里塞满了各分支之间的兼容补丁,代码注释里能看到一长串”某某写法组合曾经静默丢数据”的记录。
第二关是数值和引用的区分。IsRangeReference 判断一个值是不是单元格区域(含 ! 或者匹配 A1:B13 这类形状),IsCellReference 判断是不是单个单元格。后者的实现里有一条刻意为之的规则,注释写得很直白:光秃秃的 Q1、A1、B2 不当作单元格引用,必须带工作表前缀(Sheet1!A1 或 'My Sheet'!A1)才算。理由是这些词形和常见的字面标签(季度代号、产品名)撞车,一旦误判成引用又没有真实工作簿撑着,PowerPoint 会干脆什么都不显示。这是个典型的”宁可要求你写全,也不猜”的选择,和参数校验里那套思路一致(参见 Agent 参数校验怎么做)。
第三关是引用和缓存的并存。图表 XML 里既可以放一个指向单元格区域的引用(numRef),也可以放一份数值缓存(numCache)。代码注释记了一次真实事故:某个带引用的序列因为字面值列表被清空,生成出来的 XML 只有引用没有缓存,PowerPoint 打开后静默渲染成一张空白图。修法是把两边按位置合并——引用照写,字面值填进缓存。对你的实际影响是:如果图表指向的工作簿不在手边,读者看到的是缓存里那份数,不是引用指向的那份。dump 出来再 replay 一遍能不能对得上,全看缓存有没有被正确带出来。
还有几条硬边界值得先记住。序列键的扫描循环是 for (int i = 1; i <= 20; i++),也就是 series1 到 series20,第 21 个序列不会被点号或旧式写法读到。数值解析用的是不变文化(InvariantCulture),并且显式拒绝 NaN 和无穷;小数点只能是 .,因为 , 已经被征用作分隔符。scatter3d 和 radar3d 会被明确拒绝并抛异常,因为 OOXML 里散点和雷达就没有 3D 变体——早期版本是悄悄把 3d 抹掉降级成平面图,结果调用方以为拿到了 3D,回读也返回平面类型,属于静默背叛,现在改成了大声报错。
三、新式图表这条线:一张图带三个陪嫁文件
funnel(漏斗)、treemap(矩形树图)、sunburst(旭日图)、boxWhisker(箱线图)、histogram(直方图)、pareto(帕累托图)这六种,走的是 cx: 命名空间的另一套结构。这里的”命名空间”是 XML 里给标签分组用的前缀:老图表的标签一律带 c:,这一族一律带 cx:,两套标签属于两份不同的格式定义,元素名和嵌套规则都不通用——所以它不是”老图表加了几个新类型”,而是另起了一套。ChartExBuilder.BuildExtendedChartSpace 负责搭这棵树,读它的注释像在读一份事故复盘清单。
最要命的一类问题是”文件能生成、校验器也过、Excel 打不开”。三处注释都指向同一个症状:
cx:axisId的正确形式是带val属性的空元素,而 Open XML SDK 的类型把它建模成文本内容元素。用 SDK 的形式写出来,真实 Excel 直接拒绝打开整个工作簿(错误码 0x800A03EC),而校验器不报错。解决办法是手工构造一个原始元素塞属性进去,代价是校验器反过来误报”文本不能为空”。- 直方图的
cx:binCount/cx:binSize是同一个坑,同样的处理。 cx:chartSpace根元素上必须声明 DrawingML 的a命名空间,否则标题和数据标签那些文本元素引用了未定义的前缀,XML 本身就是坏的——同样校验器不flag,Excel 拒绝打开。
第二类是”缺零件就整块消失”。ChartExResources.cs 的类注释列了新式图表必须的三个陪嫁部件:一份内嵌的 .xlsx(被 cx:externalData 用 rId1 引用)、一份 chartStyle(cs:chartStyle id="419")、一份 colorStyle(cs:colorStyle method="cycle" id="10")。缺了它们,Office 会”修复”文件——具体表现是悄悄删掉这张图,有时连它所在的整个图形组一起删。cx:externalData 还必须是 cx:chartData 的第一个子元素。每个 cx:series 要带一个 GUID,缺了 PowerPoint 的修复逻辑会抱怨。
内嵌 xlsx 是 BuildMinimalEmbeddedXlsx 现场生成的:第 1 行 A 列留空、B 列起是序列名,第 2 行起 A 列是分类、B 列起是数值。图表 XML 里那些 Sheet1!$B$2:$B$6 形式的公式,指的就是这份内嵌表。也就是说一张新式图表其实是自带数据源的,这跟老图表指向宿主工作簿的做法不一样。
第三类是各类型自己的怪癖。箱线图必须一组数据一个 cx:data 块。一个 data 块里可以放两种维度:numDim 装数值,strDim 装文字(这里就是 X 轴上那个组名)。代码里这两者的点数被要求相等——做法是把组名重复 N 遍,凑够和数值一样多的条目,不然 Excel 会把所有箱子叠到同一个 X 位置上。另有一条同族的硬要求:每个 strDim / numDim 都得以一条指向内嵌表格的 cx:f 公式开头(箱线图是一组占内嵌表的一列,B、C、D 顺排),缺了这条公式,PowerPoint 会把整张图显示成空占位。帕累托不是一种布局,是两个序列拼出来的:主序列排序后按 clusteredColumn 画柱,第二个 paretoLine 序列靠 ownerIdx=0 从主序列派生累计百分比,绑在一根 0 到 1、单位标成 percentage 的第二值轴上;PreparePareto 会先把你的数据按值降序重排,多传的序列被直接忽略。漏斗必须配一根 id=1 的分类轴,否则同样被”修复”掉。树图和旭日图则完全没有轴。
四、预设和渲染器:一个省事的开关,一个是 Agent 的眼睛
ChartPresets.cs 只有 188 行,是这个目录里最短的两个文件之一(另一个是 154 行的 ChartExResources.cs),内容是 7 个硬编码的属性字典:minimal、dark、corporate、magazine、dashboard、colorful、monochrome(后者可写 mono)。每个就是一组普通的 key=value,比如 minimal 把网格线设成 E0E0E0:0.3、坐标轴线关掉、图例摆底部、配色定死六个十六进制值。
它的实现方式很值得看一眼:ChartHelper.Setter.cs 里遇到 preset 键时,取出对应字典,然后递归调用一次自己把这批属性套上去。属性处理有个排序函数,preset 被排在优先级 0 最先执行,这样后面你手写的任何一个属性都能覆盖预设给的值。还有一处细节:预设里带了标题样式,但图表可能压根没标题,所以套完之后会把 title. 开头的未支持项从报告里剔掉,避免因为一个”你本来就没要”的属性让整条命令非零退出。
ChartSvgRenderer.cs 是另一个极端,5635 行,加上处理新式图表的 .CxExtract.cs 共两个文件。它的作用不是生成 Office 文件,而是把图表画成 SVG,供 view <file> svg 和 watch(浏览器实时预览)这类命令使用——换句话说,它是让 Agent 和你能不打开 Office 就看见结果的那只眼睛。
这只眼睛的精度取决于它抠出来多少信息。类里那一长串属性、加上绘制入口上那些可选参数,能说明它在意什么:主次网格线开关和虚线样式、每根轴的刻度标签是否加粗倾斜、分类轴与数值轴各自的标签旋转角度、每个序列的填充透明度、被单独删掉的数据点标签集合、饼图第一块的起始角度。有几个写死的常量也值得记:主刻度线长 4 px,次网格线默认每个大格切 5 份,网格线默认线宽 0.5 px,数据标签默认 8 px。主题色的取法是读文档主题的 accent1 到 accent6,再各生成一份 50% 加深的变体,凑成 12 色循环;读不到就退回内置调色板。
有个默认值特别容易让人误判自己的数据错了:InvertIfNegative 缺省为 true,也就是负值柱会画成空心——填充色换成绘图区背景(没有背景就用白色),只保留一圈序列色的描边。这是在对齐 PowerPoint 在 c:invertIfNegative 缺失时的实际表现,不是渲染 bug。另一条注释记录了一次修正:序列填充的默认不透明度曾经是 0.85,导致每张图都比原生淡了约 15%,现在改回 1.0。
新式图表的渲染是拆开处理的:树图、旭日、箱线有各自的绘制逻辑;直方图在客户端自己算分箱、漏斗把层级当分类,两者复用普通条形图那条渲染管线。类型识别靠第一个序列的 layoutId,认不出来的一律当直方图处理——这个兜底意味着渲染器看到自己不认识的新式图表时不会崩,但也不会告诉你它猜错了。
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 老图表参数解析 | 类型/序列/分类/引用的文本解析与规范化 | src/officecli/Core/Chart/ChartHelper.cs | 每一次创建老图表 |
| 老图表构建 | 拼出 c: 命名空间的图表 XML 树 | src/officecli/Core/Chart/ChartHelper.Builder.cs | 柱/条/线/饼/散点/瀑布 |
| 老图表属性写回 | 建好之后逐属性修改(本目录最大文件,4637 行) | src/officecli/Core/Chart/ChartHelper.Setter.cs | 先建后调样式、套预设 |
| 新式图表构建 | 六种 cx: 图表的树、轴与分箱参数 | src/officecli/Core/Chart/ChartExBuilder.cs | 漏斗/树图/旭日/箱线/直方/帕累托 |
| 陪嫁资源 | 内嵌 xlsx + chartStyle + colorStyle 三件套 | src/officecli/Core/Chart/ChartExResources.cs | 新式图表被 Office 静默删掉时 |
| 样式预设 | 7 组硬编码属性字典,递归套用 | src/officecli/Core/Chart/ChartPresets.cs | 想一行搞定配色和字号 |
| SVG 渲染 | 把图表画成 SVG 供预览与自检 | src/officecli/Core/Chart/ChartSvgRenderer.cs、ChartSvgRenderer.CxExtract.cs | Agent 想”看一眼”自己画的图 |
五、边界与代价:它明确不管什么
预设是死的。 七个名字写在源码的一个 switch 里,传别的名字会抛异常并把可选值列给你。没有从配置文件加载预设的机制,也没有用户自定义预设的入口。想要第八套配色,只能自己把那十几个属性一条条写在命令行上,或者改源码重编。这个取舍的好处是行为完全可预测——预设不会因为环境不同而变——代价是团队想沉淀自己的视觉规范,得在外层自己包一层。
SVG 渲染器是近似,不是真相。 它按自己抠出来的那份信息重画一遍,而不是调用任何 Office 排版引擎。凡是提取环节没读进来的属性,渲染出来就是默认值。用它做”图表大致画对了吗”的自检很划算,用它替代”在真实 Office 里打开检查一遍”就危险了——尤其是新式图表,认不出的布局会静默按直方图处理。真要验收关键交付物,还得在真实应用里开一次。
校验器通过不等于能打开。 前面那三处注释已经说得很清楚:schema 校验和真实 Office 的接受度是两套标准,甚至互相矛盾(为了让 Excel 能打开,某些元素必须写成校验器会误报的形式)。所以 validate 命令的绿灯只是必要条件。
新式图表的可调空间比老图表小得多。 老图表那族七个文件一万三千多行里塞满了各种轴、趋势线、误差线、参考线、双轴、组合图的处理;新式图表这族的属性词汇要窄很多,树图和旭日图连轴都没有。注释里还坦白了一处:把 spPr 挂在 cx:chartSpace 上其实不在 cx schema 的子元素列表里,只是 SDK 容忍、真实 Excel 也只是忽略而非拒绝,所以为了往返一致还是照写。这是明摆着的技术债,不是干净设计。
单序列新式图表的配色,文档和代码口径不完全一致。 代码里有一条分支:只有一个序列且调色板给了多个颜色时,改成逐数据点上色。而 examples/excel/charts/charts-extended.sh 里给漏斗图的注释说的是这种情况只有第一个颜色生效、所以那里故意不传 colors=。碰到这类分歧,以你手上那版代码为准,别照着文档下结论。
这些操作动的是你磁盘上的真文件。 仓库自带的那份扩展图表示例脚本,写法是先 rm -f 掉目标文件、create 一个新的、open 进常驻,中间逐条 add / set / remove,最后 close 再 validate——全程没有自动副本这回事,你指哪个文件它就改哪个文件。README 的命令表里还有常驻模式(open 把文档保持在内存里,save 刷盘但保持常驻,close 保存并释放)和批量模式(batch 默认原子,任何一项失败整批回滚,--best-effort 才保留部分进度)。这几件事组合起来的含义要说准确:常驻模式下这个工具自己的读命令总能看到最新状态,但磁盘上的文件是延后写的——README 写明空闲后会自动刷盘(按文档保存开销自适应,2 到 10 秒),也可以用 save 立刻刷,或者把 OFFICECLI_RESIDENT_FLUSH 设成 each 让每次改动返回前就落盘。真正的坑不是”崩了全没”,而是别的程序(Word、Excel、其它脚本、上传环节)在刷盘之前去读那个文件,读到的是旧版本。批量模式则默认不会给你留半张图这种残局,但显式加了 --best-effort 就会。还有 watch 会在本机起一个 HTTP 服务做浏览器实时预览(README 示例里是 localhost 上的一个端口),批量跑生成任务时别忘了它一直开着。另外,图表可以引用外部工作簿(代码注释里出现过 [Book1.xlsx]Sheet1!$D$1:$D$3 这种形式),引用本身不会去联网抓数据,但它意味着你交出去的文件里存着一条指向别处的路径,读者看到的数则来自缓存。
六、上手清单:每条都是会真踩的
别按命名习惯猜类型名。 会踩是因为 scatter3d、radar3d 这类组合看起来完全合理,实际会抛异常;waterfall 看起来是原生类型,实际是模拟的。避法是先在 ChartHelper.cs 的 kind switch 和 ChartExBuilder.cs 的 ExtendedChartTypes 这两处把名字对一遍,或者用仓库自带的帮助数据(schemas/help/ 下按格式分目录,pptx 目录里就有 chart.json、chart-axis.json、chart-series.json 三份),那里列了这个格式实际接受的枚举值。
序列名里带冒号会踩,但已经被处理过了。 解析时是按最后一个冒号切分名字和数值的,所以 Persons (Data year: 2021):1,2,3 能正确解析。会踩的是反过来的情况:数值里出现冒号,或者你按欧洲习惯用逗号当小数点——后者会被当成分隔符,然后在数值解析处报 “Invalid data value”。避法是数值一律用 . 做小数点。
引用不写工作表名会踩。 series1.values=B2:B13 这种形状能被识别成区域引用,但单个单元格的 Q1 不会被当引用(这是刻意的)。避法是给引用一律写全 Sheet1!B2:B13,需要引号的表名(含空格、连字符、以数字开头)代码会自动加单引号,但你自己写全更保险。
新式图表生成后不验证会踩。 前面列的那些”能生成、打不开”的坑说明,这条线的失败方式是静默的:Office 不报错,只是图没了。避法是每次生成新式图表后至少做两步——validate 过一遍看结构,再用 view <file> svg 或 watch 看一眼图形本身有没有内容。两步都过还不放心的,在真实应用里开一次。
套预设的顺序会踩。 预设被排在最先执行,你手写的属性在它之后覆盖。所以 preset=dark 和 chartFill=FFFFFF 同时给,最终是白底——写命令时不用纠结先后顺序,但读别人的命令时要按这个规则去理解,不能按字面顺序理解。
拿预览图当验收标准会踩。 渲染器有自己的默认值(负值柱空心、认不出的新式图表按直方图画),你看到的可能是渲染器的行为而不是你的数据问题。避法是遇到”看起来不对”先回头看数据本身,别急着改参数——特别是负值柱变空心那个,改十遍参数也不会变实心,除非显式关掉那个开关。
批量生成时忘了失败语义会踩。 批量默认原子回滚,加了 --best-effort 才保留部分进度。Agent 跑长任务时如果为了”别整批失败”随手加上这个开关,出错后留下的就是一个半成品文件,而下一轮 Agent 大概率会在这个半成品上继续追加。避法是把”用不用 best-effort”当成一个显式决策写进 Prompt,而不是默认打开。
收尾
这套图表体系的形态,本质上是被两个约束逼出来的:OOXML 老图表规格里没有的类型只能模拟(瀑布图),新版本才有的类型必须走另一套命名空间并背上三个陪嫁文件(漏斗、树图那一族)。中间那些看起来别扭的地方——校验器和 Excel 打架、报错文案列出自己不处理的类型、单序列配色两种口径——多数是这两个约束撞在一起的产物,不是随手写坏的。
接着往下读的话,按你的目的选:想彻底搞懂老图表能调什么,去啃 ChartHelper.Setter.cs,那是本目录最大的一个文件,属性词汇几乎都在里面;想知道预览能看出什么看不出什么,读 ChartSvgRenderer.cs 开头那两百来行的属性声明就够,那批属性就是它的信息上限;想搞新式图表,把 ChartExBuilder.cs 和 ChartExResources.cs 的注释连着读一遍,两份加起来就是一张”Office 会因为什么静默删掉你的图”的清单。
自检时问自己三句:我这个类型走的是哪条线;生成完我用什么手段确认它真能打开;这次操作改的是原文件还是副本。三句都答得上来,剩下的多是调参数的事了。同一套思路用在 Excel 数据侧的自动化上,可以对照 用 AI 做 Excel 自动化 那篇一起看。
本篇属于一个把开源Office 文档读写套件 OfficeCLI逐层拆开讲的系列,整体地图见 OfficeCLI 是什么:给 AI Agent 用的开源文档读写套件;沿着这条线往下,还可以看 开源项目 OfficeCLI 的 Excel 透视表实现:五块难点拆解 和 拆解开源项目 OfficeCLI:PPT 处理器为什么比 Word 难做。