@damoqiongqiu/ice-chart-dsl 0.0.1 → 0.1.0

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/README.md CHANGED
@@ -73,11 +73,22 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
73
73
 
74
74
  ## kind 与覆盖范围
75
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 跟不上核心演进时不会把人堵死
76
+ | kind | 需要的通道 | 说明 |
77
+ | --- | --- | --- |
78
+ | `line` / `area` | `x` + `y`(+ `series`) | 数值 x 自动用数值轴 + `[x, y]` 数据点 |
79
+ | `bar` | `x` + `y`(+ `series`) | 堆叠用 `options.stack` |
80
+ | `scatter` | `x` + `y`(+ `size`) | 绑 `size` 即气泡图 |
81
+ | `pie` | `name` + `value` | 负值会被警告(饼图不表达负值) |
82
+ | `radar` | `x`(指标)+ `y`(数值)+ `series` | 指标名从 x 列推,上限自动取整到好看的刻度 |
83
+ | `heatmap` | `x` + `y`(两个类目列)+ `value` | 二维矩阵表直接画 |
84
+ | `candlestick` | `x` + `y`=**四列** `[开, 收, 低, 高]` | 少给列会明确报错 |
85
+ | `waterfall` | `name` + `value`(+ `total`) | `total` 列非 0 的行当合计项 |
86
+ | `funnel` / `gauge` / `liquid` | `name` + `value` | 仪表盘/水位球只取第一行(多行会警告) |
87
+ | `sankey` | `source` + `target` + `value` | 一张「起点 / 终点 / 流量」的连线表 |
88
+ | `function` | `expression`(+ `domain` / `params`) | 不需要 data |
89
+
90
+ **直通**:`treemap` / `graph` / `parametric` / `boxplot` 等直接给 `series`(`options` 照常透传)。
91
+ **逃生舱**:`options` 里的键覆盖编译结果(`series` 除外),所以 DSL 跟不上核心演进时不会把人堵死。
81
92
 
82
93
  ## 诊断:给 agent 的自修复反馈
83
94
 
@@ -100,6 +111,7 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
100
111
  | 导出 | 说明 |
101
112
  | --- | --- |
102
113
  | `CHART_DSL_SCHEMA_VERSION` / `CHART_DSL_KINDS` | 版本与支持的 kind |
114
+ | `CHART_DSL_COMPILED_KINDS` | 走 data/encoding 编译的 kind 清单 |
103
115
  | `validateChartDsl(dsl)` | 结构 + 语义校验,返回 `{ valid, errors, warnings }` |
104
116
  | `formatDiagnostics(result)` | 诊断 → 多行文本 |
105
117
  | `compileChartDsl(dsl)` | DSL → `ChartOption`(不合法时抛 `ChartDslCompileError`,`.diagnostics` 带原因) |
package/dist/index.cjs CHANGED
@@ -1 +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;
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]}function firstColumn(e){return Array.isArray(e)?e[0]:e}const t=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],r=["x","y","series","size","color","name","value","total","source","target"];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,l=Array.isArray(i.series)&&i.series.length>0;if(l&&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:{},l=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],l=Array.isArray(i)&&isFinite(Number(i[1]))?Number(i[1]):o[1];if(!(l>a))return[];const c=[],u=64;try{for(let i=0;i<=u;i++){const s=a+(l-a)*i/u,o={...r,[t]:s},m=e.evaluateExpression(n,o);c.push("number"==typeof m?m:NaN)}}catch(e){return[]}return c}(n,t,r,i.domain,a);for(const i of e.diagnoseExpression(n,{variable:t,params:r,values:l})){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(l)return finish(s,o);const c=resolveDataset(i);if(void 0===i.data)return fail("missing-data",`kind「${a}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(s,o);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(s,o);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(s,o);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(s,o);c.rows.forEach((e,n)=>{e.length!==c.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${n}]`)});const u=i.encoding&&"object"==typeof i.encoding?i.encoding:{};i.encoding||fail("missing-encoding",`kind「${a}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.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} 是必填的。可用列:${c.columns.join(" / ")}。`,n),-1;const r=columnIndex(c,e);return r<0?(fail("unknown-column",`列「${e}」不存在。可用列:${c.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(c,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(c,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||"funnel"===a||"gauge"===a||"liquid"===a){needColumn(u.name,"encoding.name","pie"===a||"funnel"===a);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0&&"pie"===a){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return e>=0&&("gauge"===a||"liquid"===a)&&c.rows.length>1&&warn("single-value-kind",`kind「${a}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(s,o)}if("radar"===a){const e=needColumn(u.x,"encoding.x",!0),n=needNumeric(toArray(u.y)[0],"encoding.y",!0),t=u.series?needColumn(u.series,"encoding.series",!1):-1;if(toArray(u.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),e>=0&&n>=0){const r=new Set(c.rows.map(n=>String(n[e]))),i=new Set(c.rows.map(e=>t>=0?String(e[t]):"—")),s=c.rows.filter(e=>isFinite(Number(e[n]))).length,o=r.size*i.size;r.size<3&&warn("radar-too-few-indicators",`只有 ${r.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),s<o&&warn("radar-missing-value",`有 ${o-s} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(s,o)}if("heatmap"===a)return needColumn(u.x,"encoding.x",!0),needColumn(u.y,"encoding.y",!0),needNumeric(u.value||u.color,u.value?"encoding.value":"encoding.color",!0),finish(s,o);if("candlestick"===a){needColumn(u.x,"encoding.x",!0);const e=toArray(u.y);if(4!==e.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${e.length} 列)。`,"encoding.y");else for(const n of e)needNumeric(n,"encoding.y",!0);return finish(s,o)}if("waterfall"===a)return needColumn(u.name,"encoding.name",!0),needNumeric(u.value,"encoding.value",!0),u.total&&needColumn(u.total,"encoding.total",!1),finish(s,o);if("sankey"===a){needColumn(u.source,"encoding.source",!0),needColumn(u.target,"encoding.target",!0);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<=0}).length;n&&warn("sankey-non-positive",`${n} 行流量不是正数,这些连线会被跳过。`,"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 是必填的(一个列名或一组列名)。可用列:${c.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);Object.assign(r,function compileByKind(e,n,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(e,n,t){const r=e.encoding||{},i=columnIndex(n,r.name),s=columnIndex(n,r.value),o=n.rows.map(e=>({name:toLabel(e[i]),value:toNumber(e[s])})).filter(e=>""!==e.name&&isFinite(e.value)),a="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:r.value,data:a?o.slice(0,1):o}]}}(e,n,t);case"radar":return function compileRadar(e,n){const t=e.encoding||{},r=columnIndex(n,t.x),i=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.series),o=[],a=new Map;for(const e of n.rows){const n=toLabel(e[r]),l=toNumber(e[i]);if(!n||null===l)continue;o.includes(n)||o.push(n);const c=s>=0?toLabel(e[s])||"(空)":String(firstColumn(t.y));a.has(c)||a.set(c,new Map),a.get(c).set(n,l)}const l=o.map(e=>{let n=0;for(const t of a.values())n=Math.max(n,Math.abs(t.get(e)??0));return function niceCeil(e){if(!isFinite(e)||e<=0)return 1;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<=1?1:t<=2?2:t<=5?5:10)*n}(n)});return{legend:{show:a.size>1},tooltip:{trigger:"item"},radar:{indicators:o.map((e,n)=>({name:e,max:l[n]})),splitNumber:4},series:[...a.entries()].map(([e,n],t)=>({id:`radar-${t+1}`,type:"radar",name:e,data:o.map(e=>n.get(e)??0)}))}}(e,n);case"heatmap":return function compileHeatmap(e,n){const t=e.encoding||{},r=columnIndex(n,t.x),i=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.value||t.color),o=[],a=[];for(const e of n.rows){const n=toLabel(e[r]),t=toLabel(e[i]),l=toNumber(e[s]);n&&t&&null!==l&&(o.includes(n)||o.push(n),a.push([n,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:o},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:a}]}}(e,n);case"candlestick":return function compileCandlestick(e,n){const t=e.encoding||{},r=columnIndex(n,t.x),[i,s,o,a]=toArray(t.y).map(e=>columnIndex(n,e)),l=[],c=[];for(const e of n.rows){const n=toLabel(e[r]),t=toNumber(e[i]),u=toNumber(e[s]),m=toNumber(e[o]),d=toNumber(e[a]);n&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(n)||l.push(n),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(e,n);case"waterfall":return function compileWaterfall(e,n){const t=e.encoding||{},r=columnIndex(n,t.name),i=columnIndex(n,t.value),s=columnIndex(n,t.total),o=n.rows.map(e=>{const n=toLabel(e[r]),t=toNumber(e[i]),o=s>=0?e[s]:void 0;return null!=o&&""!==o&&!1!==o&&0!==Number(o)?{name:n,value:isFinite(t)?t:0,total:!0}:{name:n,value:t}}).filter(e=>""!==e.name&&(null===e.value||isFinite(e.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:o}]}}(e,n);case"sankey":return function compileSankey(e,n){const t=e.encoding||{},r=columnIndex(n,t.source),i=columnIndex(n,t.target),s=columnIndex(n,t.value),o=[],a=[];for(const e of n.rows){const n=toLabel(e[r]),t=toLabel(e[i]),l=toNumber(e[s]);!n||!t||null===l||l<=0||(o.some(e=>e.name===n)||o.push({name:n}),o.some(e=>e.name===t)||o.push({name:t}),a.push({source:n,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:o,links:a,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(e,n);default:return 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,l=r.series?columnIndex(n,r.series):-1,c=[],pushX=e=>{c.some(n=>n===e)||c.push(e)};let u;if(l>=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[l])||"(空)";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:c.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,r)=>{const s=columnIndex(n,e),l=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:l}});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:c},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,t)}}(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","scatter","candlestick","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","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 CHANGED
@@ -1 +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};
1
+ import{diagnoseExpression as e,evaluateExpression as n,createChart as t}from"@damoqiongqiu/ice-chart";const i=1,r=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"],s=["line","area","bar","scatter","candlestick","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","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 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]}function firstColumn(e){return Array.isArray(e)?e[0]:e}const o=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],a=["x","y","series","size","color","name","value","total","source","target"];function validateChartDsl(t){const i=[],s=[],fail=(e,n,t)=>i.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>s.push({severity:"warning",code:e,message:n,path:t});if(!t||"object"!=typeof t||Array.isArray(t))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(i,s);void 0!==t.schemaVersion&&1!==t.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${t.schemaVersion}(当前是 1)。`,"schemaVersion"),t.kind?r.includes(t.kind)||fail("unsupported-kind",`不支持的 kind「${t.kind}」。可用类型:${r.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${r.join(" / ")}。`,"kind");for(const e of Object.keys(t))o.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const l=t.kind,c=Array.isArray(t.series)&&t.series.length>0;if(c&&t.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===l||"parametric"===l){const r=String(t.expression||"");if(r.trim()){const o="function"===l?"x":"t",a=t.params&&"object"==typeof t.params?t.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],l=Array.isArray(r)&&isFinite(Number(r[1]))?Number(r[1]):o[1];if(!(l>a))return[];const c=[],u=64;try{for(let r=0;r<=u;r++){const s=a+(l-a)*r/u,o={...i,[t]:s},m=n(e,o);c.push("number"==typeof m?m:NaN)}}catch(e){return[]}return c}(r,o,a,t.domain,l);for(const n of e(r,{variable:o,params:a,values:c})){const e={severity:n.severity,code:`expression-${n.code}`,message:n.message,path:"expression"};("error"===n.severity?i:s).push(e)}}else fail("missing-expression",`kind「${l}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(i,s)}if(c)return finish(i,s);const u=resolveDataset(t);if(void 0===t.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(i,s);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(i,s);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(i,s);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(i,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=t.encoding&&"object"==typeof t.encoding?t.encoding:{};t.encoding||fail("missing-encoding",`kind「${l}」需要 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,t)=>{if(!e)return t&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${u.columns.join(" / ")}。`,n),-1;const i=columnIndex(u,e);return i<0?(fail("unknown-column",`列「${e}」不存在。可用列:${u.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(u,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(u,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"===l||"funnel"===l||"gauge"===l||"liquid"===l){needColumn(m.name,"encoding.name","pie"===l||"funnel"===l);const e=needNumeric(m.value,"encoding.value",!0);if(e>=0&&"pie"===l){const n=u.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return e>=0&&("gauge"===l||"liquid"===l)&&u.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(i,s)}if("radar"===l){const e=needColumn(m.x,"encoding.x",!0),n=needNumeric(toArray(m.y)[0],"encoding.y",!0),t=m.series?needColumn(m.series,"encoding.series",!1):-1;if(toArray(m.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),e>=0&&n>=0){const i=new Set(u.rows.map(n=>String(n[e]))),r=new Set(u.rows.map(e=>t>=0?String(e[t]):"—")),s=u.rows.filter(e=>isFinite(Number(e[n]))).length,o=i.size*r.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),s<o&&warn("radar-missing-value",`有 ${o-s} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(i,s)}if("heatmap"===l)return needColumn(m.x,"encoding.x",!0),needColumn(m.y,"encoding.y",!0),needNumeric(m.value||m.color,m.value?"encoding.value":"encoding.color",!0),finish(i,s);if("candlestick"===l){needColumn(m.x,"encoding.x",!0);const e=toArray(m.y);if(4!==e.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${e.length} 列)。`,"encoding.y");else for(const n of e)needNumeric(n,"encoding.y",!0);return finish(i,s)}if("waterfall"===l)return needColumn(m.name,"encoding.name",!0),needNumeric(m.value,"encoding.value",!0),m.total&&needColumn(m.total,"encoding.total",!1),finish(i,s);if("sankey"===l){needColumn(m.source,"encoding.source",!0),needColumn(m.target,"encoding.target",!0);const e=needNumeric(m.value,"encoding.value",!0);if(e>=0){const n=u.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<=0}).length;n&&warn("sankey-non-positive",`${n} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(i,s)}if("line"===l||"area"===l||"bar"===l||"scatter"===l){"scatter"===l&&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(i,s)}return warn("passthrough-kind",`kind「${l}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(i,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?"错误":"警告",t=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${t}`});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 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);Object.assign(i,function compileByKind(e,n,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.name),s=columnIndex(n,i.value),o=n.rows.map(e=>({name:toLabel(e[r]),value:toNumber(e[s])})).filter(e=>""!==e.name&&isFinite(e.value)),a="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:a?o.slice(0,1):o}]}}(e,n,t);case"radar":return function compileRadar(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.series),o=[],a=new Map;for(const e of n.rows){const n=toLabel(e[i]),l=toNumber(e[r]);if(!n||null===l)continue;o.includes(n)||o.push(n);const c=s>=0?toLabel(e[s])||"(空)":String(firstColumn(t.y));a.has(c)||a.set(c,new Map),a.get(c).set(n,l)}const l=o.map(e=>{let n=0;for(const t of a.values())n=Math.max(n,Math.abs(t.get(e)??0));return function niceCeil(e){if(!isFinite(e)||e<=0)return 1;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<=1?1:t<=2?2:t<=5?5:10)*n}(n)});return{legend:{show:a.size>1},tooltip:{trigger:"item"},radar:{indicators:o.map((e,n)=>({name:e,max:l[n]})),splitNumber:4},series:[...a.entries()].map(([e,n],t)=>({id:`radar-${t+1}`,type:"radar",name:e,data:o.map(e=>n.get(e)??0)}))}}(e,n);case"heatmap":return function compileHeatmap(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.value||t.color),o=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[s]);n&&t&&null!==l&&(o.includes(n)||o.push(n),a.push([n,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:o},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:a}]}}(e,n);case"candlestick":return function compileCandlestick(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),[r,s,o,a]=toArray(t.y).map(e=>columnIndex(n,e)),l=[],c=[];for(const e of n.rows){const n=toLabel(e[i]),t=toNumber(e[r]),u=toNumber(e[s]),m=toNumber(e[o]),d=toNumber(e[a]);n&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(n)||l.push(n),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(e,n);case"waterfall":return function compileWaterfall(e,n){const t=e.encoding||{},i=columnIndex(n,t.name),r=columnIndex(n,t.value),s=columnIndex(n,t.total),o=n.rows.map(e=>{const n=toLabel(e[i]),t=toNumber(e[r]),o=s>=0?e[s]:void 0;return null!=o&&""!==o&&!1!==o&&0!==Number(o)?{name:n,value:isFinite(t)?t:0,total:!0}:{name:n,value:t}}).filter(e=>""!==e.name&&(null===e.value||isFinite(e.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:o}]}}(e,n);case"sankey":return function compileSankey(e,n){const t=e.encoding||{},i=columnIndex(n,t.source),r=columnIndex(n,t.target),s=columnIndex(n,t.value),o=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[s]);!n||!t||null===l||l<=0||(o.some(e=>e.name===n)||o.push({name:n}),o.some(e=>e.name===t)||o.push({name:t}),a.push({source:n,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:o,links:a,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(e,n);default:return 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,l=i.series?columnIndex(n,i.series):-1,c=[],pushX=e=>{c.some(n=>n===e)||c.push(e)};let u;if(l>=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[l])||"(空)";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:c.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,i)=>{const s=columnIndex(n,e),l=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:l}});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:c},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,t)}}(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}function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))}function renderChartDsl(e,n,i){const r=validateChartDsl(n),s=compileChartDsl(n);return{chart:t(e,s,i),option:s,diagnostics:r}}export{s as CHART_DSL_COMPILED_KINDS,r as CHART_DSL_KINDS,i as CHART_DSL_SCHEMA_VERSION,ChartDslCompileError,chartDslToJsonString,columnIndex,compileChartDsl,formatDiagnostics,numericRatio,renderChartDsl,resolveDataset,validateChartDsl};
package/dist/index.umd.js CHANGED
@@ -1 +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});
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]}function firstColumn(e){return Array.isArray(e)?e[0]:e}const i=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],r=["x","y","series","size","color","name","value","total","source","target"];function validateChartDsl(e){const o=[],s=[],fail=(e,n,t)=>o.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>s.push({severity:"warning",code:e,message:n,path:t});if(!e||"object"!=typeof e||Array.isArray(e))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(o,s);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,l=Array.isArray(e.series)&&e.series.length>0;if(l&&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:{},l=function sampleExpression(e,t,i,r,o){const s="function"===o?[-10,10]:[0,2*Math.PI],a=Array.isArray(r)&&isFinite(Number(r[0]))?Number(r[0]):s[0],l=Array.isArray(r)&&isFinite(Number(r[1]))?Number(r[1]):s[1];if(!(l>a))return[];const c=[],u=64;try{for(let r=0;r<=u;r++){const o=a+(l-a)*r/u,s={...i,[t]:o},m=n.evaluateExpression(e,s);c.push("number"==typeof m?m:NaN)}}catch(e){return[]}return c}(t,i,r,e.domain,a);for(const e of n.diagnoseExpression(t,{variable:i,params:r,values:l})){const n={severity:e.severity,code:`expression-${e.code}`,message:e.message,path:"expression"};("error"===e.severity?o:s).push(n)}}else fail("missing-expression",`kind「${a}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(o,s)}if(l)return finish(o,s);const c=resolveDataset(e);if(void 0===e.data)return fail("missing-data",`kind「${a}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(o,s);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(o,s);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(o,s);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(o,s);c.rows.forEach((e,n)=>{e.length!==c.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${n}]`)});const u=e.encoding&&"object"==typeof e.encoding?e.encoding:{};e.encoding||fail("missing-encoding",`kind「${a}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.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} 是必填的。可用列:${c.columns.join(" / ")}。`,n),-1;const i=columnIndex(c,e);return i<0?(fail("unknown-column",`列「${e}」不存在。可用列:${c.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(c,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(c,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||"funnel"===a||"gauge"===a||"liquid"===a){needColumn(u.name,"encoding.name","pie"===a||"funnel"===a);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0&&"pie"===a){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return e>=0&&("gauge"===a||"liquid"===a)&&c.rows.length>1&&warn("single-value-kind",`kind「${a}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(o,s)}if("radar"===a){const e=needColumn(u.x,"encoding.x",!0),n=needNumeric(toArray(u.y)[0],"encoding.y",!0),t=u.series?needColumn(u.series,"encoding.series",!1):-1;if(toArray(u.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),e>=0&&n>=0){const i=new Set(c.rows.map(n=>String(n[e]))),r=new Set(c.rows.map(e=>t>=0?String(e[t]):"—")),o=c.rows.filter(e=>isFinite(Number(e[n]))).length,s=i.size*r.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),o<s&&warn("radar-missing-value",`有 ${s-o} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(o,s)}if("heatmap"===a)return needColumn(u.x,"encoding.x",!0),needColumn(u.y,"encoding.y",!0),needNumeric(u.value||u.color,u.value?"encoding.value":"encoding.color",!0),finish(o,s);if("candlestick"===a){needColumn(u.x,"encoding.x",!0);const e=toArray(u.y);if(4!==e.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${e.length} 列)。`,"encoding.y");else for(const n of e)needNumeric(n,"encoding.y",!0);return finish(o,s)}if("waterfall"===a)return needColumn(u.name,"encoding.name",!0),needNumeric(u.value,"encoding.value",!0),u.total&&needColumn(u.total,"encoding.total",!1),finish(o,s);if("sankey"===a){needColumn(u.source,"encoding.source",!0),needColumn(u.target,"encoding.target",!0);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<=0}).length;n&&warn("sankey-non-positive",`${n} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(o,s)}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 是必填的(一个列名或一组列名)。可用列:${c.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(o,s)}return warn("passthrough-kind",`kind「${a}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(o,s)}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);Object.assign(i,function compileByKind(e,n,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.name),o=columnIndex(n,i.value),s=n.rows.map(e=>({name:toLabel(e[r]),value:toNumber(e[o])})).filter(e=>""!==e.name&&isFinite(e.value)),a="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:a?s.slice(0,1):s}]}}(e,n,t);case"radar":return function compileRadar(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),o=columnIndex(n,t.series),s=[],a=new Map;for(const e of n.rows){const n=toLabel(e[i]),l=toNumber(e[r]);if(!n||null===l)continue;s.includes(n)||s.push(n);const c=o>=0?toLabel(e[o])||"(空)":String(firstColumn(t.y));a.has(c)||a.set(c,new Map),a.get(c).set(n,l)}const l=s.map(e=>{let n=0;for(const t of a.values())n=Math.max(n,Math.abs(t.get(e)??0));return function niceCeil(e){if(!isFinite(e)||e<=0)return 1;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<=1?1:t<=2?2:t<=5?5:10)*n}(n)});return{legend:{show:a.size>1},tooltip:{trigger:"item"},radar:{indicators:s.map((e,n)=>({name:e,max:l[n]})),splitNumber:4},series:[...a.entries()].map(([e,n],t)=>({id:`radar-${t+1}`,type:"radar",name:e,data:s.map(e=>n.get(e)??0)}))}}(e,n);case"heatmap":return function compileHeatmap(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),o=columnIndex(n,t.value||t.color),s=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[o]);n&&t&&null!==l&&(s.includes(n)||s.push(n),a.push([n,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:s},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:a}]}}(e,n);case"candlestick":return function compileCandlestick(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),[r,o,s,a]=toArray(t.y).map(e=>columnIndex(n,e)),l=[],c=[];for(const e of n.rows){const n=toLabel(e[i]),t=toNumber(e[r]),u=toNumber(e[o]),m=toNumber(e[s]),d=toNumber(e[a]);n&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(n)||l.push(n),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(e,n);case"waterfall":return function compileWaterfall(e,n){const t=e.encoding||{},i=columnIndex(n,t.name),r=columnIndex(n,t.value),o=columnIndex(n,t.total),s=n.rows.map(e=>{const n=toLabel(e[i]),t=toNumber(e[r]),s=o>=0?e[o]:void 0;return null!=s&&""!==s&&!1!==s&&0!==Number(s)?{name:n,value:isFinite(t)?t:0,total:!0}:{name:n,value:t}}).filter(e=>""!==e.name&&(null===e.value||isFinite(e.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:s}]}}(e,n);case"sankey":return function compileSankey(e,n){const t=e.encoding||{},i=columnIndex(n,t.source),r=columnIndex(n,t.target),o=columnIndex(n,t.value),s=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[o]);!n||!t||null===l||l<=0||(s.some(e=>e.name===n)||s.push({name:n}),s.some(e=>e.name===t)||s.push({name:t}),a.push({source:n,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:s,links:a,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(e,n);default:return function compileCartesian(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.x),o=toArray(i.y),s=r>=0&&numericRatio(n,r)>.9,a=i.size?columnIndex(n,i.size):-1,l=i.series?columnIndex(n,i.series):-1,c=[],pushX=e=>{c.some(n=>n===e)||c.push(e)};let u;if(l>=0){const e=new Map,i=columnIndex(n,o[0]);for(const t of n.rows){const n=s?toNumber(t[r]):toLabel(t[r]);if(""===n||null===n)continue;pushX(n);const o=toLabel(t[l])||"(空)";e.has(o)||e.set(o,new Map),e.get(o).set(n,toNumber(t[i]))}u=[...e.entries()].map(([e,n],i)=>({id:`series-${i+1}`,type:t,name:e,data:c.map(e=>s?[e,n.get(e)??null]:n.get(e)??null)}))}else u=o.map((e,i)=>{const o=columnIndex(n,e),l=n.rows.map(e=>{const n=s?toNumber(e[r]):toLabel(e[r]),t=toNumber(e[o]);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 s?[n,t]:t}).filter(e=>null!==e);return{id:`series-${i+1}`,type:t,name:e,data:l}});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=s?{type:"value",name:i.x}:{type:"category",name:i.x,data:c},m.yAxis={name:1===o.length?o[0]:void 0},m}(e,n,t)}}(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","scatter","candlestick","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","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),o=compileChartDsl(t);return{chart:n.createChart(e,o,i),option:o,diagnostics:r}},e.resolveDataset=resolveDataset,e.validateChartDsl=validateChartDsl});
@@ -15,3 +15,9 @@ export declare function toNumber(value: ChartDslCell): number | null;
15
15
  export declare function toLabel(value: ChartDslCell): string;
16
16
  /** `y` 允许写一个列名或一组列名。 */
17
17
  export declare function toArray(value: string | string[] | undefined): string[];
18
+ /** 把一个数值向上取整到「好看的刻度」(1 / 2 / 5 × 10^k),给雷达图的指标上限用。 */
19
+ export declare function niceCeil(value: number): number;
20
+ /** 去重并保持首次出现的顺序。 */
21
+ export declare function unique<T>(values: T[]): T[];
22
+ /** `y` 这种「单列或一组列」的通道,取它当单列用时的那一列。 */
23
+ export declare function firstColumn(value: string | string[] | undefined): string | undefined;
@@ -9,10 +9,10 @@
9
9
  * 3. **结构化诊断**:列不存在 / 类型不匹配 / 空数据 / 表达式写错,都能带着位置返回给调用方去自修复。
10
10
  */
11
11
  export declare const CHART_DSL_SCHEMA_VERSION = 1;
12
- /** 支持的图表类型。带 `data`/`encoding` 编译的是前六种,其余走直通(直接给 `series`)。 */
12
+ /** 支持的图表类型。见 `CHART_DSL_COMPILED_KINDS`:没进编译清单的走直通(直接给 `series`)。 */
13
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
14
  export type ChartDslKind = (typeof CHART_DSL_KINDS)[number];
15
- /** 有 data/encoding → option 编译路径的类型。 */
15
+ /** 有 data/encoding → option 编译路径的类型(其余直通)。 */
16
16
  export declare const CHART_DSL_COMPILED_KINDS: ChartDslKind[];
17
17
  export type ChartDslCell = string | number | boolean | null | undefined;
18
18
  /**
@@ -40,6 +40,12 @@ export interface ChartDslEncoding {
40
40
  name?: string;
41
41
  /** 数值列(饼图 / 漏斗 / 仪表盘 / 水位球等单值类型)。 */
42
42
  value?: string;
43
+ /** 瀑布图:标记「合计」行的列(该行取值非空且不是 false / 0 即视为合计项)。 */
44
+ total?: string;
45
+ /** 桑基图:连线起点列。 */
46
+ source?: string;
47
+ /** 桑基图:连线终点列。 */
48
+ target?: string;
43
49
  }
44
50
  export interface ChartDslDocument {
45
51
  schemaVersion?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damoqiongqiu/ice-chart-dsl",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "author": "大漠穷秋",
6
6
  "description": "A JSON-first DSL layer for AI agents to build charts with ice-chart.",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ice-chart-dsl
3
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"
4
+ version: "0.1.0"
5
5
  category: ux
6
6
  platforms:
7
7
  - claude-code
@@ -25,6 +25,10 @@ configuration code.
25
25
  This SKILL is the right choice for:
26
26
 
27
27
  - business charts: line / area / bar / pie / scatter bubble
28
+ - tables with a second category dimension: radar (indicators), heatmap (row × column matrix)
29
+ - financial tables: candlestick (open/close/low/high), waterfall (with a total row)
30
+ - flow tables: sankey (source / target / value)
31
+ - single-value cards: funnel / gauge / liquid
28
32
  - math curves: `kind: "function"` (e.g. `sin(x)/x`, damped oscillation)
29
33
  - charts that must be interactive afterwards (hover, zoom, brush, legend toggle, serialization)
30
34
  - datasets where columns must be bound to channels instead of hand-built `series[].data`
@@ -61,7 +65,12 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
61
65
  - `encoding.y` accepts one column name or an array (each column becomes a series).
62
66
  - `encoding.series` splits one `y` column into multiple series.
63
67
  - `encoding.size` makes a bubble chart (`kind: "scatter"`).
64
- - `encoding.name` + `encoding.value` are for `pie`.
68
+ - `encoding.name` + `encoding.value` are for `pie` / `funnel` / `gauge` / `liquid`;
69
+ `waterfall` uses them plus an optional `encoding.total` column (non-zero marks the total row).
70
+ - `encoding.x` + `encoding.y` + `encoding.series` make a `radar` (x holds the indicator names).
71
+ - `encoding.x` + `encoding.y` (two category columns) + `encoding.value` make a `heatmap`.
72
+ - `encoding.x` + `encoding.y` **as four columns `[open, close, low, high]`** make a `candlestick`.
73
+ - `encoding.source` + `encoding.target` + `encoding.value` make a `sankey`.
65
74
  - `kind: "function"` needs `expression` (+ optional `domain`, `params`) and no data.
66
75
 
67
76
  ## API