@damoqiongqiu/ice-chart-dsl 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +1 -0
- package/dist/index.umd.js +1 -0
- package/dist/types/compiler/chartDslToOption.d.ts +15 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal/dataset.d.ts +17 -0
- package/dist/types/runtime/renderChartDsl.d.ts +14 -0
- package/dist/types/types.d.ts +74 -0
- package/dist/types/validate.d.ts +8 -0
- package/package.json +68 -0
- package/skills/ice-chart-dsl/SKILL.md +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 大漠穷秋
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# ice-chart-dsl
|
|
2
|
+
|
|
3
|
+
JSON-first DSL for AI agents to build charts with [`@damoqiongqiu/ice-chart`](https://www.npmjs.com/package/@damoqiongqiu/ice-chart).
|
|
4
|
+
|
|
5
|
+
ice-chart 的 `ChartOption` 本来就是纯 JSON(`toJSON()` / `fromJSONString()`),所以这个包**不是把 option 换个写法**。
|
|
6
|
+
它补的是 option 不做、而模型最容易写错的三件事:
|
|
7
|
+
|
|
8
|
+
| | ChartOption | ice-chart-dsl |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| 输入 | 自己把数据拍成 `series[].data` | **一张表 + `encoding`**:`{ x: '月份', y: '销量', series: '渠道' }` |
|
|
11
|
+
| 默认值 | 逐项自己写 | 轴类型 / 图例显隐 / 提示框触发方式按类型自动定,显式写的优先 |
|
|
12
|
+
| 出错时 | 画出来是空的,只能自己猜 | **结构化诊断**:列不存在(带可用列名)、类型不匹配、空数据、公式错误(带字符位置) |
|
|
13
|
+
|
|
14
|
+
编译产物就是普通的 `ChartOption` —— 悬停、缩放、框选、序列化、跨图联动全部照旧。
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 30 秒
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"schemaVersion": 1,
|
|
27
|
+
"kind": "line",
|
|
28
|
+
"title": "月度销量",
|
|
29
|
+
"data": {
|
|
30
|
+
"columns": ["月份", "销量", "渠道"],
|
|
31
|
+
"rows": [["1月", 120, "线上"], ["1月", 86, "线下"], ["2月", 142, "线上"], ["2月", 92, "线下"]]
|
|
32
|
+
},
|
|
33
|
+
"encoding": { "x": "月份", "y": "销量", "series": "渠道" }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { renderChartDsl, validateChartDsl, compileChartDsl } from '@damoqiongqiu/ice-chart-dsl';
|
|
39
|
+
|
|
40
|
+
const { chart, option, diagnostics } = renderChartDsl('canvas-id', dsl);
|
|
41
|
+
|
|
42
|
+
// 或者只要配置:编译出来的就是 ChartOption
|
|
43
|
+
const option = compileChartDsl(dsl);
|
|
44
|
+
|
|
45
|
+
// 或者只要诊断(validate 不抛异常,任何输入都能吃)
|
|
46
|
+
const result = validateChartDsl(dsl);
|
|
47
|
+
if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
浏览器直接用(UMD,注意顺序):
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<canvas id="chart" width="900" height="480"></canvas>
|
|
54
|
+
<script src="https://unpkg.com/ice-render/dist/index.umd.js"></script>
|
|
55
|
+
<script src="https://unpkg.com/@damoqiongqiu/ice-chart/dist/index.umd.js"></script>
|
|
56
|
+
<script src="https://unpkg.com/@damoqiongqiu/ice-chart-dsl/dist/index.umd.js"></script>
|
|
57
|
+
<script>
|
|
58
|
+
ICEChartDSL.renderChartDsl('chart', { kind: 'line', data: {...}, encoding: { x: '月份', y: '销量' } });
|
|
59
|
+
</script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 数据集两种写法
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "columns": ["月份", "销量"], "rows": [["1月", 120], ["2月", 132]] }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
[{ "月份": "1月", "销量": 120 }, { "月份": "2月", "销量": 132 }]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
对象数组是模型最常吐的形态,两种都收;`columns` 按对象 key 首次出现顺序推出来。
|
|
73
|
+
|
|
74
|
+
## kind 与覆盖范围
|
|
75
|
+
|
|
76
|
+
- **带 `data`/`encoding` 编译**:`line` / `area` / `bar` / `pie` / `scatter` / `function`
|
|
77
|
+
(`bar` 用 `options.stack` 出堆叠;`scatter` 绑 `encoding.size` 出气泡;数值 x 自动用数值轴 + `[x, y]` 数据点)
|
|
78
|
+
- **直通**:其余类型(`radar` / `heatmap` / `sankey` / `treemap` / `gauge` / `boxplot` / `waterfall` /
|
|
79
|
+
`funnel` / `graph` / `candlestick` / `parametric` / `liquid`)直接给 `series`,`options` 照常透传
|
|
80
|
+
- **逃生舱**:`options` 里的键覆盖编译结果(`series` 除外),所以 DSL 跟不上核心演进时不会把人堵死
|
|
81
|
+
|
|
82
|
+
## 诊断:给 agent 的自修复反馈
|
|
83
|
+
|
|
84
|
+
`validateChartDsl()` **不抛异常**,返回结构化诊断(`{ severity, code, message, path }`):
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
[错误] 列「销售额」不存在。可用列:月份 / 销量 / 渠道。(encoding.y)
|
|
88
|
+
[错误] 列「渠道」不是数值列(数字占比 0%),不能当 y 用。(encoding.y)
|
|
89
|
+
[错误] 第 3 行有 2 个值,但列数是 3。(data.rows[2])
|
|
90
|
+
[警告] 饼图 / 玫瑰图不适合表达负值,检测到 2 行负数。(encoding.value)
|
|
91
|
+
[错误] 表达式错误:缺少右括号(位置 6)… (expression)
|
|
92
|
+
[警告] 参数「b」定义了但表达式没有用到。(expression)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
公式类会**采样一遍再诊断**:静态检查抓语法错误,运行层抓「整段开不出来」「输出恒定」——
|
|
96
|
+
这两种恰恰是"公式写错了但没报错"最常见的表现。
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
| 导出 | 说明 |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `CHART_DSL_SCHEMA_VERSION` / `CHART_DSL_KINDS` | 版本与支持的 kind |
|
|
103
|
+
| `validateChartDsl(dsl)` | 结构 + 语义校验,返回 `{ valid, errors, warnings }` |
|
|
104
|
+
| `formatDiagnostics(result)` | 诊断 → 多行文本 |
|
|
105
|
+
| `compileChartDsl(dsl)` | DSL → `ChartOption`(不合法时抛 `ChartDslCompileError`,`.diagnostics` 带原因) |
|
|
106
|
+
| `chartDslToJsonString(dsl)` | 直接拿编译产物的 JSON 字符串 |
|
|
107
|
+
| `renderChartDsl(target, dsl, chartOptions?)` | 编译并渲染,返回 `{ chart, option, diagnostics }` |
|
|
108
|
+
| `resolveDataset` / `columnIndex` / `numericRatio` | 数据集工具(自建通道映射时用得上) |
|
|
109
|
+
|
|
110
|
+
JSON Schema:[`src/schema/chart-dsl.schema.json`](./src/schema/chart-dsl.schema.json)。
|
|
111
|
+
|
|
112
|
+
## Example
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm install
|
|
116
|
+
npm run build
|
|
117
|
+
# 起一个静态服务打开 examples/chart-dsl.html
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
左边写 DSL,右边实时渲染,下面是诊断面板(含「错误示例」预设,可以直接看到列名纠错长什么样)。
|
|
121
|
+
|
|
122
|
+
## Agent discovery
|
|
123
|
+
|
|
124
|
+
- npm 包导出(`validateChartDsl` / `compileChartDsl` / `renderChartDsl`)
|
|
125
|
+
- [`skills/ice-chart-dsl/SKILL.md`](./skills/ice-chart-dsl/SKILL.md)
|
|
126
|
+
- [`prompts/agent-prompt.md`](./prompts/agent-prompt.md)
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("@damoqiongqiu/ice-chart");const n=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(e){const n=e&&e.data;if(!n)return null;if(Array.isArray(n)){const e=n.filter(e=>e&&"object"==typeof e&&!Array.isArray(e));if(!e.length)return{columns:[],rows:[]};const t=[];for(const n of e)for(const e of Object.keys(n))t.includes(e)||t.push(e);return{columns:t,rows:e.map(e=>t.map(n=>e[n]))}}if("object"!=typeof n)return null;const t=Array.isArray(n.columns)?n.columns.map(e=>String(e)):[],r=Array.isArray(n.rows)?n.rows:[];return t.length?{columns:t,rows:r.map(e=>Array.isArray(e)?e:[])}:null}function columnIndex(e,n){return e&&n?e.columns.indexOf(n):-1}function isNumericCell(e){return"number"==typeof e?isFinite(e):"string"==typeof e&&""!==e.trim()&&isFinite(Number(e))}function numericRatio(e,n){let t=0,r=0;for(const i of e.rows){const e=i[n];null!=e&&""!==e&&(t+=1,isNumericCell(e)&&(r+=1))}return t?r/t:0}function toNumber(e){if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e&&""!==e.trim()){const n=Number(e);return isFinite(n)?n:null}return null}function toLabel(e){return null==e?"":String(e)}function toArray(e){return null==e?[]:Array.isArray(e)?e.filter(e=>"string"==typeof e&&e):[e]}const t=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],r=["x","y","series","size","color","name","value"];function validateChartDsl(i){const s=[],o=[],fail=(e,n,t)=>s.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>o.push({severity:"warning",code:e,message:n,path:t});if(!i||"object"!=typeof i||Array.isArray(i))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(s,o);void 0!==i.schemaVersion&&1!==i.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${i.schemaVersion}(当前是 1)。`,"schemaVersion"),i.kind?n.includes(i.kind)||fail("unsupported-kind",`不支持的 kind「${i.kind}」。可用类型:${n.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${n.join(" / ")}。`,"kind");for(const e of Object.keys(i))t.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const a=i.kind,c=Array.isArray(i.series)&&i.series.length>0;if(c&&i.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===a||"parametric"===a){const n=String(i.expression||"");if(n.trim()){const t="function"===a?"x":"t",r=i.params&&"object"==typeof i.params?i.params:{},c=function sampleExpression(n,t,r,i,s){const o="function"===s?[-10,10]:[0,2*Math.PI],a=Array.isArray(i)&&isFinite(Number(i[0]))?Number(i[0]):o[0],c=Array.isArray(i)&&isFinite(Number(i[1]))?Number(i[1]):o[1];if(!(c>a))return[];const l=[],u=64;try{for(let i=0;i<=u;i++){const s=a+(c-a)*i/u,o={...r,[t]:s},m=e.evaluateExpression(n,o);l.push("number"==typeof m?m:NaN)}}catch(e){return[]}return l}(n,t,r,i.domain,a);for(const i of e.diagnoseExpression(n,{variable:t,params:r,values:c})){const e={severity:i.severity,code:`expression-${i.code}`,message:i.message,path:"expression"};("error"===i.severity?s:o).push(e)}}else fail("missing-expression",`kind「${a}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(s,o)}if(c)return finish(s,o);const l=resolveDataset(i);if(void 0===i.data)return fail("missing-data",`kind「${a}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(s,o);if(!l)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(s,o);if(!l.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(s,o);if(!l.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(s,o);l.rows.forEach((e,n)=>{e.length!==l.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${l.columns.length}(columns:${l.columns.join(" / ")})。`,`data.rows[${n}]`)});const u=i.encoding&&"object"==typeof i.encoding?i.encoding:{};i.encoding||fail("missing-encoding",`kind「${a}」需要 encoding 把列绑到通道上,例如 { "x": "${l.columns[0]}", "y": "${l.columns[1]||l.columns[0]}" }。`,"encoding");for(const e of Object.keys(u))r.includes(e)||warn("unknown-encoding",`encoding 里的未知通道「${e}」会被忽略。`,`encoding.${e}`);const needColumn=(e,n,t)=>{if(!e)return t&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${l.columns.join(" / ")}。`,n),-1;const r=columnIndex(l,e);return r<0?(fail("unknown-column",`列「${e}」不存在。可用列:${l.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(l,r)?r:(fail("empty-column",`列「${e}」整列都是空值。`,n),-1)},needNumeric=(e,n,t)=>{const r=needColumn(e,n,t);if(r<0)return-1;const i=numericRatio(l,r);return i<.6?(fail("non-numeric-column",`列「${e}」不是数值列(数字占比 ${(100*i).toFixed(0)}%),不能当 ${n.split(".")[1]} 用。`,n),-1):(i<1&&warn("partly-non-numeric",`列「${e}」有 ${Math.round(100*(1-i))}% 的值不是数字,这些点会被当作空缺。`,n),r)};if("pie"===a){needColumn(u.name,"encoding.name",!0);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0){const n=l.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return finish(s,o)}if("line"===a||"area"===a||"bar"===a||"scatter"===a){"scatter"===a&&u.size&&needNumeric(u.size,"encoding.size",!1);const e=toArray(u.y);e.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${l.columns.join(" / ")}。`,"encoding.y"),e.length>8&&warn("too-many-series",`y 绑定了 ${e.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(u.x,"encoding.x",!0);for(const n of e)needNumeric(n,"encoding.y",!0);return u.series&&needColumn(u.series,"encoding.series",!1),finish(s,o)}return warn("passthrough-kind",`kind「${a}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(s,o)}function finish(e,n){return{valid:0===e.length,errors:e,warnings:n}}class ChartDslCompileError extends Error{constructor(e){super(`[ice-chart-dsl] DSL 校验不通过:\n${e.map(e=>`- ${e.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=e}}function compileChartDsl(e){const n=validateChartDsl(e);if(!n.valid)throw new ChartDslCompileError(n.errors);const t=e.kind,r={title:normalizeTitle(e),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(e.series)&&e.series.length)r.series=e.series;else if("function"===t||"parametric"===t)r.legend={show:!1},r.tooltip={trigger:"item"},r.crosshair={show:!0,axis:"x",showAxisLabel:!0},r.xAxis={type:"value",name:"x"},r.yAxis={name:"y"},r.series=[{id:"curve",type:t,name:e.title&&"object"==typeof e.title?e.title.text:String(e.title||("function"===t?"f(x)":"curve")),expression:e.expression,domain:e.domain,params:e.params,lineWidth:2}];else{const n=resolveDataset(e);"pie"===t?Object.assign(r,function compilePie(e,n){const t=e.encoding||{},r=columnIndex(n,t.name),i=columnIndex(n,t.value),s=n.rows.map(e=>({name:toLabel(e[r]),value:toNumber(e[i])})).filter(e=>""!==e.name&&isFinite(e.value));return{legend:{show:!0},tooltip:{trigger:"item"},series:[{id:"pie",type:"pie",name:t.value,data:s}]}}(e,n)):Object.assign(r,function compileCartesian(e,n,t){const r=e.encoding||{},i=columnIndex(n,r.x),s=toArray(r.y),o=i>=0&&numericRatio(n,i)>.9,a=r.size?columnIndex(n,r.size):-1,c=r.series?columnIndex(n,r.series):-1,l=[],pushX=e=>{l.some(n=>n===e)||l.push(e)};let u;if(c>=0){const e=new Map,r=columnIndex(n,s[0]);for(const t of n.rows){const n=o?toNumber(t[i]):toLabel(t[i]);if(""===n||null===n)continue;pushX(n);const s=toLabel(t[c])||"(空)";e.has(s)||e.set(s,new Map),e.get(s).set(n,toNumber(t[r]))}u=[...e.entries()].map(([e,n],r)=>({id:`series-${r+1}`,type:t,name:e,data:l.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,r)=>{const s=columnIndex(n,e),c=n.rows.map(e=>{const n=o?toNumber(e[i]):toLabel(e[i]),t=toNumber(e[s]);if(""===n||null===n||null===t)return null;if(pushX(n),a>=0){const r=toNumber(e[a]);return[n,t,null===r?8:r]}return o?[n,t]:t}).filter(e=>null!==e);return{id:`series-${r+1}`,type:t,name:e,data:c}});const m={legend:{show:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=o?{type:"value",name:r.x}:{type:"category",name:r.x,data:l},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,t))}if(e.options&&"object"==typeof e.options)for(const n of Object.keys(e.options)){const t=e.options[n];void 0!==t&&("series"!==n&&(r[n]=t))}return r}function normalizeTitle(e){if(e.title)return"string"==typeof e.title?{text:e.title}:e.title}exports.CHART_DSL_COMPILED_KINDS=["line","area","bar","pie","scatter","function"],exports.CHART_DSL_KINDS=n,exports.CHART_DSL_SCHEMA_VERSION=1,exports.ChartDslCompileError=ChartDslCompileError,exports.chartDslToJsonString=function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))},exports.columnIndex=columnIndex,exports.compileChartDsl=compileChartDsl,exports.formatDiagnostics=function formatDiagnostics(e){const n=[...e.errors,...e.warnings].map(e=>{const n="error"===e.severity?"错误":"警告",t=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${t}`});return n.length?n.join("\n"):"✓ DSL 校验通过"},exports.numericRatio=numericRatio,exports.renderChartDsl=function renderChartDsl(n,t,r){const i=validateChartDsl(t),s=compileChartDsl(t);return{chart:e.createChart(n,s,r),option:s,diagnostics:i}},exports.resolveDataset=resolveDataset,exports.validateChartDsl=validateChartDsl;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{diagnoseExpression as e,evaluateExpression as n,createChart as i}from"@damoqiongqiu/ice-chart";const t=1,r=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"],s=["line","area","bar","pie","scatter","function"];function resolveDataset(e){const n=e&&e.data;if(!n)return null;if(Array.isArray(n)){const e=n.filter(e=>e&&"object"==typeof e&&!Array.isArray(e));if(!e.length)return{columns:[],rows:[]};const i=[];for(const n of e)for(const e of Object.keys(n))i.includes(e)||i.push(e);return{columns:i,rows:e.map(e=>i.map(n=>e[n]))}}if("object"!=typeof n)return null;const i=Array.isArray(n.columns)?n.columns.map(e=>String(e)):[],t=Array.isArray(n.rows)?n.rows:[];return i.length?{columns:i,rows:t.map(e=>Array.isArray(e)?e:[])}:null}function columnIndex(e,n){return e&&n?e.columns.indexOf(n):-1}function isNumericCell(e){return"number"==typeof e?isFinite(e):"string"==typeof e&&""!==e.trim()&&isFinite(Number(e))}function numericRatio(e,n){let i=0,t=0;for(const r of e.rows){const e=r[n];null!=e&&""!==e&&(i+=1,isNumericCell(e)&&(t+=1))}return i?t/i:0}function toNumber(e){if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e&&""!==e.trim()){const n=Number(e);return isFinite(n)?n:null}return null}function toLabel(e){return null==e?"":String(e)}function toArray(e){return null==e?[]:Array.isArray(e)?e.filter(e=>"string"==typeof e&&e):[e]}const o=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],a=["x","y","series","size","color","name","value"];function validateChartDsl(i){const t=[],s=[],fail=(e,n,i)=>t.push({severity:"error",code:e,message:n,path:i}),warn=(e,n,i)=>s.push({severity:"warning",code:e,message:n,path:i});if(!i||"object"!=typeof i||Array.isArray(i))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(t,s);void 0!==i.schemaVersion&&1!==i.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${i.schemaVersion}(当前是 1)。`,"schemaVersion"),i.kind?r.includes(i.kind)||fail("unsupported-kind",`不支持的 kind「${i.kind}」。可用类型:${r.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${r.join(" / ")}。`,"kind");for(const e of Object.keys(i))o.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const c=i.kind,l=Array.isArray(i.series)&&i.series.length>0;if(l&&i.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===c||"parametric"===c){const r=String(i.expression||"");if(r.trim()){const o="function"===c?"x":"t",a=i.params&&"object"==typeof i.params?i.params:{},l=function sampleExpression(e,i,t,r,s){const o="function"===s?[-10,10]:[0,2*Math.PI],a=Array.isArray(r)&&isFinite(Number(r[0]))?Number(r[0]):o[0],c=Array.isArray(r)&&isFinite(Number(r[1]))?Number(r[1]):o[1];if(!(c>a))return[];const l=[],u=64;try{for(let r=0;r<=u;r++){const s=a+(c-a)*r/u,o={...t,[i]:s},m=n(e,o);l.push("number"==typeof m?m:NaN)}}catch(e){return[]}return l}(r,o,a,i.domain,c);for(const n of e(r,{variable:o,params:a,values:l})){const e={severity:n.severity,code:`expression-${n.code}`,message:n.message,path:"expression"};("error"===n.severity?t:s).push(e)}}else fail("missing-expression",`kind「${c}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(t,s)}if(l)return finish(t,s);const u=resolveDataset(i);if(void 0===i.data)return fail("missing-data",`kind「${c}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(t,s);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(t,s);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(t,s);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(t,s);u.rows.forEach((e,n)=>{e.length!==u.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${n}]`)});const m=i.encoding&&"object"==typeof i.encoding?i.encoding:{};i.encoding||fail("missing-encoding",`kind「${c}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const e of Object.keys(m))a.includes(e)||warn("unknown-encoding",`encoding 里的未知通道「${e}」会被忽略。`,`encoding.${e}`);const needColumn=(e,n,i)=>{if(!e)return i&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${u.columns.join(" / ")}。`,n),-1;const t=columnIndex(u,e);return t<0?(fail("unknown-column",`列「${e}」不存在。可用列:${u.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const i=e[n];return null!=i&&""!==i})}(u,t)?t:(fail("empty-column",`列「${e}」整列都是空值。`,n),-1)},needNumeric=(e,n,i)=>{const t=needColumn(e,n,i);if(t<0)return-1;const r=numericRatio(u,t);return r<.6?(fail("non-numeric-column",`列「${e}」不是数值列(数字占比 ${(100*r).toFixed(0)}%),不能当 ${n.split(".")[1]} 用。`,n),-1):(r<1&&warn("partly-non-numeric",`列「${e}」有 ${Math.round(100*(1-r))}% 的值不是数字,这些点会被当作空缺。`,n),t)};if("pie"===c){needColumn(m.name,"encoding.name",!0);const e=needNumeric(m.value,"encoding.value",!0);if(e>=0){const n=u.rows.filter(n=>{const i=Number(n[e]);return isFinite(i)&&i<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return finish(t,s)}if("line"===c||"area"===c||"bar"===c||"scatter"===c){"scatter"===c&&m.size&&needNumeric(m.size,"encoding.size",!1);const e=toArray(m.y);e.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),e.length>8&&warn("too-many-series",`y 绑定了 ${e.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(m.x,"encoding.x",!0);for(const n of e)needNumeric(n,"encoding.y",!0);return m.series&&needColumn(m.series,"encoding.series",!1),finish(t,s)}return warn("passthrough-kind",`kind「${c}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(t,s)}function finish(e,n){return{valid:0===e.length,errors:e,warnings:n}}function formatDiagnostics(e){const n=[...e.errors,...e.warnings].map(e=>{const n="error"===e.severity?"错误":"警告",i=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${i}`});return n.length?n.join("\n"):"✓ DSL 校验通过"}class ChartDslCompileError extends Error{constructor(e){super(`[ice-chart-dsl] DSL 校验不通过:\n${e.map(e=>`- ${e.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=e}}function compileChartDsl(e){const n=validateChartDsl(e);if(!n.valid)throw new ChartDslCompileError(n.errors);const i=e.kind,t={title:normalizeTitle(e),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(e.series)&&e.series.length)t.series=e.series;else if("function"===i||"parametric"===i)t.legend={show:!1},t.tooltip={trigger:"item"},t.crosshair={show:!0,axis:"x",showAxisLabel:!0},t.xAxis={type:"value",name:"x"},t.yAxis={name:"y"},t.series=[{id:"curve",type:i,name:e.title&&"object"==typeof e.title?e.title.text:String(e.title||("function"===i?"f(x)":"curve")),expression:e.expression,domain:e.domain,params:e.params,lineWidth:2}];else{const n=resolveDataset(e);"pie"===i?Object.assign(t,function compilePie(e,n){const i=e.encoding||{},t=columnIndex(n,i.name),r=columnIndex(n,i.value),s=n.rows.map(e=>({name:toLabel(e[t]),value:toNumber(e[r])})).filter(e=>""!==e.name&&isFinite(e.value));return{legend:{show:!0},tooltip:{trigger:"item"},series:[{id:"pie",type:"pie",name:i.value,data:s}]}}(e,n)):Object.assign(t,function compileCartesian(e,n,i){const t=e.encoding||{},r=columnIndex(n,t.x),s=toArray(t.y),o=r>=0&&numericRatio(n,r)>.9,a=t.size?columnIndex(n,t.size):-1,c=t.series?columnIndex(n,t.series):-1,l=[],pushX=e=>{l.some(n=>n===e)||l.push(e)};let u;if(c>=0){const e=new Map,t=columnIndex(n,s[0]);for(const i of n.rows){const n=o?toNumber(i[r]):toLabel(i[r]);if(""===n||null===n)continue;pushX(n);const s=toLabel(i[c])||"(空)";e.has(s)||e.set(s,new Map),e.get(s).set(n,toNumber(i[t]))}u=[...e.entries()].map(([e,n],t)=>({id:`series-${t+1}`,type:i,name:e,data:l.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,t)=>{const s=columnIndex(n,e),c=n.rows.map(e=>{const n=o?toNumber(e[r]):toLabel(e[r]),i=toNumber(e[s]);if(""===n||null===n||null===i)return null;if(pushX(n),a>=0){const t=toNumber(e[a]);return[n,i,null===t?8:t]}return o?[n,i]:i}).filter(e=>null!==e);return{id:`series-${t+1}`,type:i,name:e,data:c}});const m={legend:{show:u.length>1},tooltip:{trigger:"scatter"===i?"item":"axis"},series:u};m.crosshair="scatter"===i?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=o?{type:"value",name:t.x}:{type:"category",name:t.x,data:l},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,i))}if(e.options&&"object"==typeof e.options)for(const n of Object.keys(e.options)){const i=e.options[n];void 0!==i&&("series"!==n&&(t[n]=i))}return t}function normalizeTitle(e){if(e.title)return"string"==typeof e.title?{text:e.title}:e.title}function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))}function renderChartDsl(e,n,t){const r=validateChartDsl(n),s=compileChartDsl(n);return{chart:i(e,s,t),option:s,diagnostics:r}}export{s as CHART_DSL_COMPILED_KINDS,r as CHART_DSL_KINDS,t as CHART_DSL_SCHEMA_VERSION,ChartDslCompileError,chartDslToJsonString,columnIndex,compileChartDsl,formatDiagnostics,numericRatio,renderChartDsl,resolveDataset,validateChartDsl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("@damoqiongqiu/ice-chart")):"function"==typeof define&&define.amd?define(["exports","@damoqiongqiu/ice-chart"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ICEChartDSL={},e.ICEChart)}(this,function(e,n){"use strict";const t=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(e){const n=e&&e.data;if(!n)return null;if(Array.isArray(n)){const e=n.filter(e=>e&&"object"==typeof e&&!Array.isArray(e));if(!e.length)return{columns:[],rows:[]};const t=[];for(const n of e)for(const e of Object.keys(n))t.includes(e)||t.push(e);return{columns:t,rows:e.map(e=>t.map(n=>e[n]))}}if("object"!=typeof n)return null;const t=Array.isArray(n.columns)?n.columns.map(e=>String(e)):[],i=Array.isArray(n.rows)?n.rows:[];return t.length?{columns:t,rows:i.map(e=>Array.isArray(e)?e:[])}:null}function columnIndex(e,n){return e&&n?e.columns.indexOf(n):-1}function isNumericCell(e){return"number"==typeof e?isFinite(e):"string"==typeof e&&""!==e.trim()&&isFinite(Number(e))}function numericRatio(e,n){let t=0,i=0;for(const r of e.rows){const e=r[n];null!=e&&""!==e&&(t+=1,isNumericCell(e)&&(i+=1))}return t?i/t:0}function toNumber(e){if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e&&""!==e.trim()){const n=Number(e);return isFinite(n)?n:null}return null}function toLabel(e){return null==e?"":String(e)}function toArray(e){return null==e?[]:Array.isArray(e)?e.filter(e=>"string"==typeof e&&e):[e]}const i=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],r=["x","y","series","size","color","name","value"];function validateChartDsl(e){const s=[],o=[],fail=(e,n,t)=>s.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>o.push({severity:"warning",code:e,message:n,path:t});if(!e||"object"!=typeof e||Array.isArray(e))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(s,o);void 0!==e.schemaVersion&&1!==e.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${e.schemaVersion}(当前是 1)。`,"schemaVersion"),e.kind?t.includes(e.kind)||fail("unsupported-kind",`不支持的 kind「${e.kind}」。可用类型:${t.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${t.join(" / ")}。`,"kind");for(const n of Object.keys(e))i.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const a=e.kind,c=Array.isArray(e.series)&&e.series.length>0;if(c&&e.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===a||"parametric"===a){const t=String(e.expression||"");if(t.trim()){const i="function"===a?"x":"t",r=e.params&&"object"==typeof e.params?e.params:{},c=function sampleExpression(e,t,i,r,s){const o="function"===s?[-10,10]:[0,2*Math.PI],a=Array.isArray(r)&&isFinite(Number(r[0]))?Number(r[0]):o[0],c=Array.isArray(r)&&isFinite(Number(r[1]))?Number(r[1]):o[1];if(!(c>a))return[];const l=[],u=64;try{for(let r=0;r<=u;r++){const s=a+(c-a)*r/u,o={...i,[t]:s},m=n.evaluateExpression(e,o);l.push("number"==typeof m?m:NaN)}}catch(e){return[]}return l}(t,i,r,e.domain,a);for(const e of n.diagnoseExpression(t,{variable:i,params:r,values:c})){const n={severity:e.severity,code:`expression-${e.code}`,message:e.message,path:"expression"};("error"===e.severity?s:o).push(n)}}else fail("missing-expression",`kind「${a}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(s,o)}if(c)return finish(s,o);const l=resolveDataset(e);if(void 0===e.data)return fail("missing-data",`kind「${a}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(s,o);if(!l)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(s,o);if(!l.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(s,o);if(!l.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(s,o);l.rows.forEach((e,n)=>{e.length!==l.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${l.columns.length}(columns:${l.columns.join(" / ")})。`,`data.rows[${n}]`)});const u=e.encoding&&"object"==typeof e.encoding?e.encoding:{};e.encoding||fail("missing-encoding",`kind「${a}」需要 encoding 把列绑到通道上,例如 { "x": "${l.columns[0]}", "y": "${l.columns[1]||l.columns[0]}" }。`,"encoding");for(const e of Object.keys(u))r.includes(e)||warn("unknown-encoding",`encoding 里的未知通道「${e}」会被忽略。`,`encoding.${e}`);const needColumn=(e,n,t)=>{if(!e)return t&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${l.columns.join(" / ")}。`,n),-1;const i=columnIndex(l,e);return i<0?(fail("unknown-column",`列「${e}」不存在。可用列:${l.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(l,i)?i:(fail("empty-column",`列「${e}」整列都是空值。`,n),-1)},needNumeric=(e,n,t)=>{const i=needColumn(e,n,t);if(i<0)return-1;const r=numericRatio(l,i);return r<.6?(fail("non-numeric-column",`列「${e}」不是数值列(数字占比 ${(100*r).toFixed(0)}%),不能当 ${n.split(".")[1]} 用。`,n),-1):(r<1&&warn("partly-non-numeric",`列「${e}」有 ${Math.round(100*(1-r))}% 的值不是数字,这些点会被当作空缺。`,n),i)};if("pie"===a){needColumn(u.name,"encoding.name",!0);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0){const n=l.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return finish(s,o)}if("line"===a||"area"===a||"bar"===a||"scatter"===a){"scatter"===a&&u.size&&needNumeric(u.size,"encoding.size",!1);const e=toArray(u.y);e.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${l.columns.join(" / ")}。`,"encoding.y"),e.length>8&&warn("too-many-series",`y 绑定了 ${e.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(u.x,"encoding.x",!0);for(const n of e)needNumeric(n,"encoding.y",!0);return u.series&&needColumn(u.series,"encoding.series",!1),finish(s,o)}return warn("passthrough-kind",`kind「${a}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(s,o)}function finish(e,n){return{valid:0===e.length,errors:e,warnings:n}}class ChartDslCompileError extends Error{constructor(e){super(`[ice-chart-dsl] DSL 校验不通过:\n${e.map(e=>`- ${e.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=e}}function compileChartDsl(e){const n=validateChartDsl(e);if(!n.valid)throw new ChartDslCompileError(n.errors);const t=e.kind,i={title:normalizeTitle(e),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(e.series)&&e.series.length)i.series=e.series;else if("function"===t||"parametric"===t)i.legend={show:!1},i.tooltip={trigger:"item"},i.crosshair={show:!0,axis:"x",showAxisLabel:!0},i.xAxis={type:"value",name:"x"},i.yAxis={name:"y"},i.series=[{id:"curve",type:t,name:e.title&&"object"==typeof e.title?e.title.text:String(e.title||("function"===t?"f(x)":"curve")),expression:e.expression,domain:e.domain,params:e.params,lineWidth:2}];else{const n=resolveDataset(e);"pie"===t?Object.assign(i,function compilePie(e,n){const t=e.encoding||{},i=columnIndex(n,t.name),r=columnIndex(n,t.value),s=n.rows.map(e=>({name:toLabel(e[i]),value:toNumber(e[r])})).filter(e=>""!==e.name&&isFinite(e.value));return{legend:{show:!0},tooltip:{trigger:"item"},series:[{id:"pie",type:"pie",name:t.value,data:s}]}}(e,n)):Object.assign(i,function compileCartesian(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.x),s=toArray(i.y),o=r>=0&&numericRatio(n,r)>.9,a=i.size?columnIndex(n,i.size):-1,c=i.series?columnIndex(n,i.series):-1,l=[],pushX=e=>{l.some(n=>n===e)||l.push(e)};let u;if(c>=0){const e=new Map,i=columnIndex(n,s[0]);for(const t of n.rows){const n=o?toNumber(t[r]):toLabel(t[r]);if(""===n||null===n)continue;pushX(n);const s=toLabel(t[c])||"(空)";e.has(s)||e.set(s,new Map),e.get(s).set(n,toNumber(t[i]))}u=[...e.entries()].map(([e,n],i)=>({id:`series-${i+1}`,type:t,name:e,data:l.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,i)=>{const s=columnIndex(n,e),c=n.rows.map(e=>{const n=o?toNumber(e[r]):toLabel(e[r]),t=toNumber(e[s]);if(""===n||null===n||null===t)return null;if(pushX(n),a>=0){const i=toNumber(e[a]);return[n,t,null===i?8:i]}return o?[n,t]:t}).filter(e=>null!==e);return{id:`series-${i+1}`,type:t,name:e,data:c}});const m={legend:{show:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=o?{type:"value",name:i.x}:{type:"category",name:i.x,data:l},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,t))}if(e.options&&"object"==typeof e.options)for(const n of Object.keys(e.options)){const t=e.options[n];void 0!==t&&("series"!==n&&(i[n]=t))}return i}function normalizeTitle(e){if(e.title)return"string"==typeof e.title?{text:e.title}:e.title}e.CHART_DSL_COMPILED_KINDS=["line","area","bar","pie","scatter","function"],e.CHART_DSL_KINDS=t,e.CHART_DSL_SCHEMA_VERSION=1,e.ChartDslCompileError=ChartDslCompileError,e.chartDslToJsonString=function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))},e.columnIndex=columnIndex,e.compileChartDsl=compileChartDsl,e.formatDiagnostics=function formatDiagnostics(e){const n=[...e.errors,...e.warnings].map(e=>{const n="error"===e.severity?"错误":"警告",t=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${t}`});return n.length?n.join("\n"):"✓ DSL 校验通过"},e.numericRatio=numericRatio,e.renderChartDsl=function renderChartDsl(e,t,i){const r=validateChartDsl(t),s=compileChartDsl(t);return{chart:n.createChart(e,s,i),option:s,diagnostics:r}},e.resolveDataset=resolveDataset,e.validateChartDsl=validateChartDsl});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChartOption } from '@damoqiongqiu/ice-chart';
|
|
2
|
+
import type { ChartDslDiagnostic, ChartDslDocument } from '../types';
|
|
3
|
+
/** 编译失败时抛出:`diagnostics` 就是校验结果,调用方可以直接拿去做自修复提示。 */
|
|
4
|
+
export declare class ChartDslCompileError extends Error {
|
|
5
|
+
diagnostics: ChartDslDiagnostic[];
|
|
6
|
+
constructor(diagnostics: ChartDslDiagnostic[]);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* DSL → ChartOption(纯 JSON,可直接喂给 `ICEChart.createChart`)。
|
|
10
|
+
*
|
|
11
|
+
* 显式写的永远优先:`options` 里的键会覆盖编译结果,`series` 直通时完全跳过编译。
|
|
12
|
+
*/
|
|
13
|
+
export declare function compileChartDsl(dsl: ChartDslDocument): ChartOption;
|
|
14
|
+
/** 把 DSL 直接编译成 JSON 字符串(存盘 / 进日志 / 给别的进程用)。 */
|
|
15
|
+
export declare function chartDslToJsonString(dsl: ChartDslDocument): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export { validateChartDsl, formatDiagnostics } from './validate';
|
|
3
|
+
export { compileChartDsl, chartDslToJsonString, ChartDslCompileError } from './compiler/chartDslToOption';
|
|
4
|
+
export { renderChartDsl, type RenderChartDslResult } from './runtime/renderChartDsl';
|
|
5
|
+
export { resolveDataset, columnIndex, numericRatio } from './internal/dataset';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChartDslCell, ChartDslDocument } from '../types';
|
|
2
|
+
export interface ResolvedDataset {
|
|
3
|
+
columns: string[];
|
|
4
|
+
rows: ChartDslCell[][];
|
|
5
|
+
}
|
|
6
|
+
/** 把两种数据集写法归一成 `{ columns, rows }`;拿不到有效数据返回 null。 */
|
|
7
|
+
export declare function resolveDataset(dsl: ChartDslDocument | null | undefined): ResolvedDataset | null;
|
|
8
|
+
export declare function columnIndex(dataset: ResolvedDataset | null, name: string | undefined): number;
|
|
9
|
+
export declare function isNumericCell(value: ChartDslCell): boolean;
|
|
10
|
+
/** 一列里「非空值的数字占比」,用来判断这列是不是数值列。 */
|
|
11
|
+
export declare function numericRatio(dataset: ResolvedDataset, index: number): number;
|
|
12
|
+
/** 一列是否有任何非空值。 */
|
|
13
|
+
export declare function hasValue(dataset: ResolvedDataset, index: number): boolean;
|
|
14
|
+
export declare function toNumber(value: ChartDslCell): number | null;
|
|
15
|
+
export declare function toLabel(value: ChartDslCell): string;
|
|
16
|
+
/** `y` 允许写一个列名或一组列名。 */
|
|
17
|
+
export declare function toArray(value: string | string[] | undefined): string[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ChartOption, type ICEChart } from '@damoqiongqiu/ice-chart';
|
|
2
|
+
import type { ChartDslDocument, ChartDslValidationResult } from '../types';
|
|
3
|
+
export interface RenderChartDslResult {
|
|
4
|
+
chart: ICEChart;
|
|
5
|
+
/** 编译出来的 ChartOption:可以直接 `chart.toJSON()` 存盘,或再喂给别的画布。 */
|
|
6
|
+
option: ChartOption;
|
|
7
|
+
/** 校验结果(含 warning):编译能过但值得提醒的地方都在这里。 */
|
|
8
|
+
diagnostics: ChartDslValidationResult;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 渲染 DSL。校验不通过会抛 `ChartDslCompileError`(带结构化诊断),
|
|
12
|
+
* 想「先看诊断再决定要不要画」就先调 `validateChartDsl`。
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderChartDsl(target: string | HTMLCanvasElement, dsl: ChartDslDocument, chartOptions?: Record<string, any>): RenderChartDslResult;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表 DSL 的类型定义。
|
|
3
|
+
*
|
|
4
|
+
* 设计前提:ice-chart 的 `ChartOption` **本来就是纯 JSON**(`toJSON()` / `fromJSONString()`
|
|
5
|
+
* 已经吃掉了序列化契约),所以这个 DSL 不是「把 option 换个写法」,它补的是三件 option 不做的事:
|
|
6
|
+
*
|
|
7
|
+
* 1. **数据绑定**:给一张表(列名 + 行)和 `encoding`(x / y / 分组列),编译出 option;
|
|
8
|
+
* 2. **意图级默认**:轴类型、图例显隐、提示框触发方式按图表类型自动定,显式写的永远优先;
|
|
9
|
+
* 3. **结构化诊断**:列不存在 / 类型不匹配 / 空数据 / 表达式写错,都能带着位置返回给调用方去自修复。
|
|
10
|
+
*/
|
|
11
|
+
export declare const CHART_DSL_SCHEMA_VERSION = 1;
|
|
12
|
+
/** 支持的图表类型。带 `data`/`encoding` 编译的是前六种,其余走直通(直接给 `series`)。 */
|
|
13
|
+
export declare const CHART_DSL_KINDS: readonly ["line", "area", "bar", "pie", "scatter", "function", "radar", "candlestick", "heatmap", "sankey", "treemap", "gauge", "boxplot", "waterfall", "funnel", "graph", "parametric", "liquid"];
|
|
14
|
+
export type ChartDslKind = (typeof CHART_DSL_KINDS)[number];
|
|
15
|
+
/** 有 data/encoding → option 编译路径的类型。 */
|
|
16
|
+
export declare const CHART_DSL_COMPILED_KINDS: ChartDslKind[];
|
|
17
|
+
export type ChartDslCell = string | number | boolean | null | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* 表格式数据集。两种写法都收:
|
|
20
|
+
* - `{ columns: ['月份', '销量'], rows: [['1月', 120]] }`
|
|
21
|
+
* - `[{ 月份: '1月', 销量: 120 }]`(JSON 对象数组 —— 模型最常吐的形态)
|
|
22
|
+
*/
|
|
23
|
+
export type ChartDslDataset = {
|
|
24
|
+
columns: string[];
|
|
25
|
+
rows: ChartDslCell[][];
|
|
26
|
+
} | Array<Record<string, ChartDslCell>>;
|
|
27
|
+
/** 列名 → 视觉通道的绑定。 */
|
|
28
|
+
export interface ChartDslEncoding {
|
|
29
|
+
/** 横轴:类目列、时间列或数值列(数值列自动用数值轴 + [x, y] 数据点)。 */
|
|
30
|
+
x?: string;
|
|
31
|
+
/** 纵轴:一个列名或一组列名,每个列名编译成一个系列。 */
|
|
32
|
+
y?: string | string[];
|
|
33
|
+
/** 分组列:同一个 x 上的取值拆成多个系列。 */
|
|
34
|
+
series?: string;
|
|
35
|
+
/** 第三维:气泡大小。 */
|
|
36
|
+
size?: string;
|
|
37
|
+
/** 逐项配色 / 热力图数值列。 */
|
|
38
|
+
color?: string;
|
|
39
|
+
/** 名称列(饼图 / 漏斗 / 桑基等按名称分项的类型)。 */
|
|
40
|
+
name?: string;
|
|
41
|
+
/** 数值列(饼图 / 漏斗 / 仪表盘 / 水位球等单值类型)。 */
|
|
42
|
+
value?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ChartDslDocument {
|
|
45
|
+
schemaVersion?: number;
|
|
46
|
+
kind: ChartDslKind;
|
|
47
|
+
data?: ChartDslDataset;
|
|
48
|
+
encoding?: ChartDslEncoding;
|
|
49
|
+
/** 标题:字符串等价于 `{ text }`。 */
|
|
50
|
+
title?: string | {
|
|
51
|
+
text?: string;
|
|
52
|
+
subtext?: string;
|
|
53
|
+
};
|
|
54
|
+
/** `kind: 'function'` 的表达式与参数(y = f(x))。 */
|
|
55
|
+
expression?: string;
|
|
56
|
+
domain?: [number, number];
|
|
57
|
+
params?: Record<string, number>;
|
|
58
|
+
/** 逃生舱:直接并进 ChartOption 顶层(同名键覆盖编译结果)。 */
|
|
59
|
+
options?: Record<string, any>;
|
|
60
|
+
/** 逃生舱:直接给 `option.series`(给了它就不再走 data/encoding)。 */
|
|
61
|
+
series?: any[];
|
|
62
|
+
}
|
|
63
|
+
export interface ChartDslDiagnostic {
|
|
64
|
+
severity: 'error' | 'warning';
|
|
65
|
+
code: string;
|
|
66
|
+
message: string;
|
|
67
|
+
/** 出问题的位置,如 `encoding.y` / `data.rows[3]` / `expression`。 */
|
|
68
|
+
path?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ChartDslValidationResult {
|
|
71
|
+
valid: boolean;
|
|
72
|
+
errors: ChartDslDiagnostic[];
|
|
73
|
+
warnings: ChartDslDiagnostic[];
|
|
74
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ChartDslDocument, type ChartDslValidationResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 结构 + 语义校验。**任何输入都不抛异常**(包括 null / 数组 / 乱七八糟的对象),
|
|
4
|
+
* 因为它是给 agent 做自修复用的反馈通道。
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateChartDsl(dsl: ChartDslDocument | any): ChartDslValidationResult;
|
|
7
|
+
/** 把诊断渲染成人类 / agent 都能读的多行文本。 */
|
|
8
|
+
export declare function formatDiagnostics(result: ChartDslValidationResult): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@damoqiongqiu/ice-chart-dsl",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "大漠穷秋",
|
|
6
|
+
"description": "A JSON-first DSL layer for AI agents to build charts with ice-chart.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsl",
|
|
9
|
+
"chart",
|
|
10
|
+
"json",
|
|
11
|
+
"ai-agent",
|
|
12
|
+
"ice-chart",
|
|
13
|
+
"visualization"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ice-render/ice-chart-dsl.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/ice-render/ice-chart-dsl#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/ice-render/ice-chart-dsl/issues"
|
|
22
|
+
},
|
|
23
|
+
"main": "dist/index.cjs",
|
|
24
|
+
"module": "dist/index.mjs",
|
|
25
|
+
"types": "dist/types/index.d.ts",
|
|
26
|
+
"browser": "dist/index.umd.js",
|
|
27
|
+
"unpkg": "dist/index.umd.js",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"skills",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "npm run clean && npm run build:types && npm run build:js",
|
|
37
|
+
"build:js": "rollup -c --environment NODE_ENV:production",
|
|
38
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
39
|
+
"clean": "rimraf dist",
|
|
40
|
+
"dev": "rollup -c -w --environment NODE_ENV:development",
|
|
41
|
+
"test": "jest --runInBand",
|
|
42
|
+
"types:check": "tsc --noEmit",
|
|
43
|
+
"verify": "npm run types:check && npm run build && npm test -- --runInBand",
|
|
44
|
+
"prepublishOnly": "npm run verify"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@damoqiongqiu/ice-chart": "^0.17.2",
|
|
48
|
+
"ice-render": "^1.4.7"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@babel/core": "^7.28.5",
|
|
52
|
+
"@babel/preset-env": "^7.28.5",
|
|
53
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
54
|
+
"@damoqiongqiu/ice-chart": "^0.17.2",
|
|
55
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
56
|
+
"@rollup/plugin-commonjs": "^28.0.1",
|
|
57
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
58
|
+
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
59
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
60
|
+
"@types/jest": "^29.5.14",
|
|
61
|
+
"babel-jest": "^29.7.0",
|
|
62
|
+
"ice-render": "^1.4.7",
|
|
63
|
+
"jest": "^29.7.0",
|
|
64
|
+
"rimraf": "^6.0.1",
|
|
65
|
+
"rollup": "^3.29.5",
|
|
66
|
+
"typescript": "^5.9.3"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ice-chart-dsl
|
|
3
|
+
description: Build interactive ice-chart charts from a JSON-first DSL — a table plus an encoding that binds columns to channels, with structured diagnostics for self-repair.
|
|
4
|
+
version: "0.0.1"
|
|
5
|
+
category: ux
|
|
6
|
+
platforms:
|
|
7
|
+
- claude-code
|
|
8
|
+
- codex-cli
|
|
9
|
+
- copilot
|
|
10
|
+
- cursor
|
|
11
|
+
- gemini-cli
|
|
12
|
+
- other
|
|
13
|
+
metadata:
|
|
14
|
+
short-description: JSON-first DSL for ice-chart charts — data binding, intent defaults, and actionable validation.
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# ice-chart-dsl
|
|
18
|
+
|
|
19
|
+
Use this skill when the user has data (a table, CSV rows, or a JSON array) and wants a chart
|
|
20
|
+
rendered by `ice-chart`, and the chart should be produced as **data** rather than imperative
|
|
21
|
+
configuration code.
|
|
22
|
+
|
|
23
|
+
## Capability boundary
|
|
24
|
+
|
|
25
|
+
This SKILL is the right choice for:
|
|
26
|
+
|
|
27
|
+
- business charts: line / area / bar / pie / scatter bubble
|
|
28
|
+
- math curves: `kind: "function"` (e.g. `sin(x)/x`, damped oscillation)
|
|
29
|
+
- charts that must be interactive afterwards (hover, zoom, brush, legend toggle, serialization)
|
|
30
|
+
- datasets where columns must be bound to channels instead of hand-built `series[].data`
|
|
31
|
+
|
|
32
|
+
Use something else when:
|
|
33
|
+
|
|
34
|
+
- the user wants a generic node/edge diagram (use the ice-render DSL family)
|
|
35
|
+
- the chart type is outside the compiled set and the caller already has a full `ChartOption`
|
|
36
|
+
(the DSL still works — pass `series` through — but there is no added value)
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Document shape
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"schemaVersion": 1,
|
|
49
|
+
"kind": "line | area | bar | pie | scatter | function | (passthrough kinds)",
|
|
50
|
+
"title": "可选",
|
|
51
|
+
"data": { "columns": ["月份", "销量", "渠道"], "rows": [["1月", 120, "线上"]] },
|
|
52
|
+
"encoding": { "x": "月份", "y": "销量", "series": "渠道" },
|
|
53
|
+
"options": { "stack": true },
|
|
54
|
+
"expression": "a*sin(x)/x",
|
|
55
|
+
"domain": [-10, 10],
|
|
56
|
+
"params": { "a": 1.5 }
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `data` accepts `{ columns, rows }` **or** an array of objects (`[{ "月份": "1月", "销量": 120 }]`).
|
|
61
|
+
- `encoding.y` accepts one column name or an array (each column becomes a series).
|
|
62
|
+
- `encoding.series` splits one `y` column into multiple series.
|
|
63
|
+
- `encoding.size` makes a bubble chart (`kind: "scatter"`).
|
|
64
|
+
- `encoding.name` + `encoding.value` are for `pie`.
|
|
65
|
+
- `kind: "function"` needs `expression` (+ optional `domain`, `params`) and no data.
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { validateChartDsl, compileChartDsl, renderChartDsl } from '@damoqiongqiu/ice-chart-dsl';
|
|
71
|
+
|
|
72
|
+
const { valid, errors, warnings } = validateChartDsl(dsl); // never throws
|
|
73
|
+
const option = compileChartDsl(dsl); // throws ChartDslCompileError on errors
|
|
74
|
+
const { chart, option } = renderChartDsl('canvas-id', dsl);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Self-repair loop
|
|
78
|
+
|
|
79
|
+
1. Write the DSL.
|
|
80
|
+
2. Call `validateChartDsl`. Treat every `error` as a compile failure.
|
|
81
|
+
3. Read the message — it names the available columns, the numeric ratio, or the character
|
|
82
|
+
position of a formula error — and fix that field.
|
|
83
|
+
4. Re-validate. Mention `warning`s to the user (e.g. pie with negative values, unused params).
|
|
84
|
+
|
|
85
|
+
Example feedback:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
[错误] 列「销售额」不存在。可用列:月份 / 销量 / 渠道。(encoding.y)
|
|
89
|
+
[错误] 表达式错误:缺少右括号(位置 6)。(expression)
|
|
90
|
+
[警告] 参数「b」定义了但表达式没有用到。(expression)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Reference
|
|
94
|
+
|
|
95
|
+
- package: `@damoqiongqiu/ice-chart-dsl` (npm)
|
|
96
|
+
- runtime dependency: `@damoqiongqiu/ice-chart` + `ice-render`
|
|
97
|
+
- schema: `src/schema/chart-dsl.schema.json`
|