@damoqiongqiu/ice-chart-dsl 0.6.3 → 0.6.5

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
@@ -79,6 +79,9 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
79
79
  | `bar` | `x` + `y`(+ `series`) | 堆叠用 `options.stack` |
80
80
  | `scatter` | `x` + `y`(+ `size`) | 绑 `size` 即气泡图 |
81
81
  | `beeswarm` | `x`(分组)+ `y`(观测值) | 逐点编成 `[组, 值]`,同一组内自动避让;提示框按数据项触发 |
82
+ | `hexbin` | `x` + `y`(都要数值,+ 可选 `size` 当权重) | 两列点表直接编成蜂窝分箱;十字准星 xy,提示框按格子 |
83
+ | `calendar` | `x`(日期列)+ `y`(数值列) | 一行一天的热力格;日期按 **UTC 的 Y/M/D** 对齐(跨时区一致),同一天多条取和 |
84
+ | `alluvial` | `axes`(≥ 2 个轴列)+ 可选 `value` | **表的分面**(对比桑基是图的拓扑):一行记录在每个轴上取一个类目;不绑 `value` 就按每条记录算 1 |
82
85
  | `violin` | `x`(分组)+ `y`(观测值) | 按组把观测值收成 `number[][]`,core 算密度轮廓;多组对比用 `series` 拆系列 |
83
86
  | `pie` | `name` + `value` | 负值会被警告(饼图不表达负值) |
84
87
  | `radar` | `x`(指标)+ `y`(数值)+ `series` | 指标名从 x 列推,上限自动取整到好看的刻度 |
@@ -89,6 +92,7 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
89
92
  | `function` | `expression`(+ `domain` / `params`) | 不需要 data |
90
93
 
91
94
  **直通**:`treemap` / `graph` / `parametric` / `boxplot` 等直接给 `series`(`options` 照常透传)。
95
+ 分箱的半径与配色(`hexbin.radius` / `aggregate` / 配色)也走直通或 `options` —— 那属于 ChartOption 的活。
92
96
  **逃生舱**:`options` 里的键覆盖编译结果(`series` 除外),所以 DSL 跟不上核心演进时不会把人堵死。
93
97
 
94
98
  **面板矩阵(数据驱动分面)**:顶层加 `matrix: { rows, columns, gap? }`,DSL 会把
@@ -108,6 +112,40 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
108
112
  面板数,都会给可执行的警告(`matrix-single-panel` / `matrix-too-few-panels`)。
109
113
  分布组图的细调(`violin.bandwidth` / `beeswarm.spread` 等)走 `series` 直通 —— 那属于 ChartOption 的活。
110
114
 
115
+ ## 日历热力与多轴分类流
116
+
117
+ 两块**自带坐标语义**的类型(不画直角坐标轴):日历的「轴」是日历本身,多轴分类流有 N 个类目轴。
118
+ 它们的**列名照旧写在 `encoding` 里**,只有与数据无关的观感配置走各自顶层的同名字段。
119
+
120
+ ```json
121
+ {
122
+ "kind": "calendar",
123
+ "data": { "columns": ["日期", "提交数"], "rows": [["2026-01-01", 3], ["2026-01-02", 5]] },
124
+ "encoding": { "x": "日期", "y": "提交数" },
125
+ "calendar": { "weekStart": 0, "maxColor": "#a21caf" }
126
+ }
127
+ ```
128
+
129
+ ```json
130
+ {
131
+ "kind": "alluvial",
132
+ "data": {
133
+ "columns": ["渠道", "地区", "品类"],
134
+ "rows": [["直营网店", "华东", "手机"], ["直播带货", "华南", "配件"]]
135
+ },
136
+ "encoding": { "axes": ["渠道", "地区", "品类"] },
137
+ "alluvial": { "sort": "total", "nodeWidth": 14 }
138
+ }
139
+ ```
140
+
141
+ - `calendar`:`encoding.x` 是日期列、`encoding.y` 是数值列(一行一天)。日期**不在 DSL 里解析**
142
+ (core 按 UTC 的 Y/M/D 对齐、同一天多条取和),所以 `Date` 与 `2026-1-5` 这类写法都还能用;
143
+ 读不出来的格子只会得到 `calendar-unparseable-date` **警告**(带上行数),不会挡住编译。
144
+ - `alluvial`:`encoding.axes` 按顺序列出轴列(至少两个),`encoding.value` 是流量列;
145
+ 不绑 `value` 时按每条记录算 1 —— core 的 `alluvial.rows` 是记录对象数组,这个转换在编译期做,
146
+ 调用方不必自己把表拍成对象。轴少于两个 / `axes` 写成单个列名 / 轴列不存在都是**错误**
147
+ (诊断里带可用列名);单类目轴与「行里缺轴值」只是**警告** —— 后者正是 core 会静默跳过的那些行。
148
+
111
149
  ## 标注:目标线 / 阈值线 / 异常点 / 目标区间
112
150
 
113
151
  业务图上最高频的参考线不是「一种新图表」,而是一条配置 —— 顶层 `annotation`(与 `encoding` 平级):
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var n=require("@damoqiongqiu/ice-chart");const e=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","violin","beeswarm","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function unique(n){const e=[];for(const t of n)e.some(n=>n===t)||e.push(t);return e}function firstColumn(n){return Array.isArray(n)?n[0]:n}const t=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series","matrix"],i=["x","y","series","size","color","name","value","total","source","target"],o=["line","area","bar","scatter","violin","beeswarm"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(o){const r=[],a=[],fail=(n,e,t)=>r.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>a.push({severity:"warning",code:n,message:e,path:t});if(!o||"object"!=typeof o||Array.isArray(o))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(r,a);void 0!==o.schemaVersion&&1!==o.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${o.schemaVersion}(当前是 1)。`,"schemaVersion"),o.kind?e.includes(o.kind)||fail("unsupported-kind",`不支持的 kind「${o.kind}」。可用类型:${e.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${e.join(" / ")}。`,"kind");for(const n of Object.keys(o))t.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const s=o.kind,l=Array.isArray(o.series)&&o.series.length>0;if(l&&o.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===s||"parametric"===s){const e=String(o.expression||"");if(e.trim()){const t="function"===s?"x":"t",i=o.params&&"object"==typeof o.params?o.params:{},l=function sampleExpression(e,t,i,o,r){const a="function"===r?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):a[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):a[1];if(!(l>s))return[];const c=[],u=64;try{for(let o=0;o<=u;o++){const r=s+(l-s)*o/u,a={...i,[t]:r},m=n.evaluateExpression(e,a);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(e,t,i,o.domain,s);for(const o of n.diagnoseExpression(e,{variable:t,params:i,values:l})){const n={severity:o.severity,code:`expression-${o.code}`,message:o.message,path:"expression"};("error"===o.severity?r:a).push(n)}}else fail("missing-expression",`kind「${s}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(r,a)}if(l)return validateAnnotation(o.annotation,{kind:s,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(r,a);const c=resolveDataset(o);if(void 0===o.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(r,a);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(r,a);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(r,a);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(r,a);c.rows.forEach((n,e)=>{n.length!==c.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${e}]`)});const u=o.encoding&&"object"==typeof o.encoding?o.encoding:{};o.encoding||fail("missing-encoding",`kind「${s}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.columns[0]}" }。`,"encoding");for(const n of Object.keys(u))i.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const m=u.x?columnIndex(c,u.x):-1;validateAnnotation(o.annotation,{kind:s,dataset:c,xIndex:m,numericX:m>=0&&numericRatio(c,m)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${c.columns.join(" / ")}。`,e),-1;const i=columnIndex(c,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${c.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(c,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(c,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if(function validateMatrix(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-matrix","matrix 需要 { rows, columns, gap? }:rows / columns 是数字(等分)或权重数组。","matrix");const o=n,validSize=n=>"number"==typeof n&&isFinite(n)&&n>0||Array.isArray(n)&&n.length>0&&n.every(n=>"number"==typeof n&&isFinite(n)&&n>0);validSize(o.rows)||t("invalid-matrix","matrix.rows 要是正数(等分几行)或正数数组(每行的权重)。","matrix.rows");validSize(o.columns)||t("invalid-matrix","matrix.columns 要是正数(等分几列)或正数数组(每列的权重)。","matrix.columns");void 0!==o.gap&&("number"!=typeof o.gap||!isFinite(o.gap)||o.gap<0)&&t("invalid-matrix","matrix.gap 要是非负数字(面板间距,设备像素)。","matrix.gap");if(!e.cartesian)return void i("matrix-ignored",`kind「${e.kind}」不是直角坐标场景,matrix 会被忽略(面板矩阵只在直角坐标下生效)。`,"matrix");const r=("number"==typeof o.rows?Math.floor(o.rows):Array.isArray(o.rows)?o.rows.length:0)*("number"==typeof o.columns?Math.floor(o.columns):Array.isArray(o.columns)?o.columns.length:0);e.seriesCount<=1?i("matrix-single-panel","matrix 只拆出一块面板:分面靠 encoding.series(每个分组一块面板)或多个 y 列,至少要两个。","matrix"):r&&e.seriesCount>r&&i("matrix-too-few-panels",`${e.seriesCount} 个分组只有 ${r} 块面板,多出来的会挤在最后一块上。`,"matrix")}(o.matrix,{kind:s,cartesian:"line"===s||"area"===s||"bar"===s||"scatter"===s||"violin"===s||"beeswarm"===s,seriesCount:u.series&&columnIndex(c,u.series)>=0?unique(c.rows.map(n=>toLabel(n[columnIndex(c,u.series)])).filter(n=>""!==n)).length:Math.max(1,toArray(u.y).length)},fail,warn),"pie"===s||"funnel"===s||"gauge"===s||"liquid"===s){needColumn(u.name,"encoding.name","pie"===s||"funnel"===s);const n=needNumeric(u.value,"encoding.value",!0);if(n>=0&&"pie"===s){const e=c.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===s||"liquid"===s)&&c.rows.length>1&&warn("single-value-kind",`kind「${s}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(r,a)}if("radar"===s){const n=needColumn(u.x,"encoding.x",!0),e=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"),n>=0&&e>=0){const i=new Set(c.rows.map(e=>String(e[n]))),o=new Set(c.rows.map(n=>t>=0?String(n[t]):"—")),r=c.rows.filter(n=>isFinite(Number(n[e]))).length,a=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),r<a&&warn("radar-missing-value",`有 ${a-r} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(r,a)}if("heatmap"===s)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(r,a);if("waterfall"===s)return needColumn(u.name,"encoding.name",!0),needNumeric(u.value,"encoding.value",!0),u.total&&needColumn(u.total,"encoding.total",!1),finish(r,a);if("sankey"===s){needColumn(u.source,"encoding.source",!0),needColumn(u.target,"encoding.target",!0);const n=needNumeric(u.value,"encoding.value",!0);if(n>=0){const e=c.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(r,a)}if("violin"===s||"beeswarm"===s){const n=toArray(u.y);return n.length?(needNumeric(n[0],"encoding.y",!0),"violin"===s&&n.length>1&&warn("violin-single-y",`violin 的 y 只认第一列「${n[0]}」:多组对比请用 encoding.series 拆系列,或改用 beeswarm。`,"encoding.y")):fail("missing-encoding-channel",`encoding.y 是必填的(观测值那一列)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),needColumn(u.x,"encoding.x",!1),finish(r,a)}if("line"===s||"area"===s||"bar"===s||"scatter"===s){"scatter"===s&&u.size&&needNumeric(u.size,"encoding.size",!1);const n=toArray(u.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(u.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return u.series&&needColumn(u.series,"encoding.series",!1),finish(r,a)}return warn("passthrough-kind",`kind「${s}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(r,a)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const r=o.includes(e.kind);r||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${o.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,o)=>{if(r&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${o} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,o);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${o} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,o)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${o} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,o)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const r="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,r,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,r,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.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:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),r=columnIndex(e,i.value),a=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[r])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?a.slice(0,1):a}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.series),a=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;a.includes(e)||a.push(e);const c=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=a.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:a.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:a.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.value||t.color),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);e&&t&&null!==l&&(a.includes(e)||a.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:a},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),r=columnIndex(e,t.total),a=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),a=r>=0?n[r]:void 0;return null!=a&&""!==a&&!1!==a&&0!==Number(a)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.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:a}]}}(n,e);case"violin":return function compileViolin(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=t.series?columnIndex(e,t.series):-1,a=[];for(const n of e.rows){const e=i>=0?toLabel(n[i]):"全部";e&&!a.includes(e)&&a.push(e)}const collect=n=>{const e=new Map;for(const t of n){const n=i>=0?toLabel(t[i]):"全部";if(!n)continue;const r=toNumber(t[o]);null!==r&&(e.has(n)||e.set(n,[]),e.get(n).push(r))}return a.map(n=>e.get(n)||[])};let s;if(r>=0){const n=new Map;for(const t of e.rows){const e=toLabel(t[r])||"(空)";n.has(e)||n.set(e,[]),n.get(e).push(t)}s=[...n.entries()].map(([n,e],t)=>({id:`series-${t+1}`,type:"violin",name:n,data:collect(e)}))}else s=[{id:"series-1",type:"violin",name:firstColumn(t.y),data:collect(e.rows)}];return{legend:{show:s.length>1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:a},yAxis:{name:firstColumn(t.y)},series:s}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),r=columnIndex(e,t.value),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);!e||!t||null===l||l<=0||(a.some(n=>n.name===e)||a.push({name:e}),a.some(n=>n.name===t)||a.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:a,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),r=toArray(i.y),a=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,c="scatter"===t||"beeswarm"===t,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let m;if(l>=0){const n=new Map,i=columnIndex(e,r[0]);for(const t of e.rows){const e=a?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const r=toLabel(t[l])||"(空)";n.has(r)||n.set(r,new Map),n.get(r).set(e,toNumber(t[i]))}m=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else m=r.map((n,i)=>{const r=columnIndex(e,n),l=e.rows.map(n=>{const e=a?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[r]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return a||c?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const d={legend:{show:m.length>1},tooltip:{trigger:c?"item":"axis"},series:m};d.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return d.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},d.yAxis={name:1===r.length?r[0]:void 0},d}(n,e,t)}}(n,e,t))}if(n.matrix&&function isCartesianKind(n){return"line"===n||"area"===n||"bar"===n||"scatter"===n||"violin"===n||"beeswarm"===n}(t)){i.matrix=n.matrix;const e=Array.isArray(i.series)?i.series:[];e.length>1&&e.forEach((n,e)=>{n&&"object"==typeof n&&(n.panel=e)})}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}exports.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","waterfall","heatmap","violin","beeswarm","sankey","pie","radar","funnel","gauge","liquid","function"],exports.CHART_DSL_KINDS=e,exports.CHART_DSL_SCHEMA_VERSION=1,exports.ChartDslCompileError=ChartDslCompileError,exports.chartDslToJsonString=function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))},exports.columnIndex=columnIndex,exports.compileChartDsl=compileChartDsl,exports.formatDiagnostics=function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"},exports.numericRatio=numericRatio,exports.renderChartDsl=function renderChartDsl(e,t,i){const o=validateChartDsl(t),r=compileChartDsl(t);return{chart:n.createChart(e,r,i),option:r,diagnostics:o}},exports.resolveDataset=resolveDataset,exports.validateChartDsl=validateChartDsl;
1
+ "use strict";var n=require("@damoqiongqiu/ice-chart");const e=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","violin","beeswarm","hexbin","calendar","alluvial","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function unique(n){const e=[];for(const t of n)e.some(n=>n===t)||e.push(t);return e}function firstColumn(n){return Array.isArray(n)?n[0]:n}const t=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series","matrix","calendar","alluvial"],i=["x","y","series","size","color","name","value","total","source","target","axes"],o=["line","area","bar","scatter","violin","beeswarm","hexbin"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(r){const a=[],s=[],fail=(n,e,t)=>a.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>s.push({severity:"warning",code:n,message:e,path:t});if(!r||"object"!=typeof r||Array.isArray(r))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(a,s);void 0!==r.schemaVersion&&1!==r.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${r.schemaVersion}(当前是 1)。`,"schemaVersion"),r.kind?e.includes(r.kind)||fail("unsupported-kind",`不支持的 kind「${r.kind}」。可用类型:${e.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${e.join(" / ")}。`,"kind");for(const n of Object.keys(r))t.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const l=r.kind,c=Array.isArray(r.series)&&r.series.length>0;if(c&&r.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===l||"parametric"===l){const e=String(r.expression||"");if(e.trim()){const t="function"===l?"x":"t",i=r.params&&"object"==typeof r.params?r.params:{},o=function sampleExpression(e,t,i,o,r){const a="function"===r?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):a[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):a[1];if(!(l>s))return[];const c=[],u=64;try{for(let o=0;o<=u;o++){const r=s+(l-s)*o/u,a={...i,[t]:r},m=n.evaluateExpression(e,a);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(e,t,i,r.domain,l);for(const r of n.diagnoseExpression(e,{variable:t,params:i,values:o})){const n={severity:r.severity,code:`expression-${r.code}`,message:r.message,path:"expression"};("error"===r.severity?a:s).push(n)}}else fail("missing-expression",`kind「${l}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(a,s)}if(c)return validateAnnotation(r.annotation,{kind:l,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(a,s);const u=resolveDataset(r);if(void 0===r.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(a,s);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(a,s);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(a,s);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(a,s);u.rows.forEach((n,e)=>{n.length!==u.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${e}]`)});const m=r.encoding&&"object"==typeof r.encoding?r.encoding:{};r.encoding||fail("missing-encoding",`kind「${l}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const n of Object.keys(m))i.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const d=m.x?columnIndex(u,m.x):-1;validateAnnotation(r.annotation,{kind:l,dataset:u,xIndex:d,numericX:d>=0&&numericRatio(u,d)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${u.columns.join(" / ")}。`,e),-1;const i=columnIndex(u,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${u.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(u,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(u,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if(function validateMatrix(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-matrix","matrix 需要 { rows, columns, gap? }:rows / columns 是数字(等分)或权重数组。","matrix");const o=n,validSize=n=>"number"==typeof n&&isFinite(n)&&n>0||Array.isArray(n)&&n.length>0&&n.every(n=>"number"==typeof n&&isFinite(n)&&n>0);validSize(o.rows)||t("invalid-matrix","matrix.rows 要是正数(等分几行)或正数数组(每行的权重)。","matrix.rows");validSize(o.columns)||t("invalid-matrix","matrix.columns 要是正数(等分几列)或正数数组(每列的权重)。","matrix.columns");void 0!==o.gap&&("number"!=typeof o.gap||!isFinite(o.gap)||o.gap<0)&&t("invalid-matrix","matrix.gap 要是非负数字(面板间距,设备像素)。","matrix.gap");if(!e.cartesian)return void i("matrix-ignored",`kind「${e.kind}」不是直角坐标场景,matrix 会被忽略(面板矩阵只在直角坐标下生效)。`,"matrix");const r=("number"==typeof o.rows?Math.floor(o.rows):Array.isArray(o.rows)?o.rows.length:0)*("number"==typeof o.columns?Math.floor(o.columns):Array.isArray(o.columns)?o.columns.length:0);e.seriesCount<=1?i("matrix-single-panel","matrix 只拆出一块面板:分面靠 encoding.series(每个分组一块面板)或多个 y 列,至少要两个。","matrix"):r&&e.seriesCount>r&&i("matrix-too-few-panels",`${e.seriesCount} 个分组只有 ${r} 块面板,多出来的会挤在最后一块上。`,"matrix")}(r.matrix,{kind:l,cartesian:o.includes(l),seriesCount:m.series&&columnIndex(u,m.series)>=0?unique(u.rows.map(n=>toLabel(n[columnIndex(u,m.series)])).filter(n=>""!==n)).length:Math.max(1,toArray(m.y).length)},fail,warn),"pie"===l||"funnel"===l||"gauge"===l||"liquid"===l){needColumn(m.name,"encoding.name","pie"===l||"funnel"===l);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0&&"pie"===l){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===l||"liquid"===l)&&u.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(a,s)}if("radar"===l){const n=needColumn(m.x,"encoding.x",!0),e=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"),n>=0&&e>=0){const i=new Set(u.rows.map(e=>String(e[n]))),o=new Set(u.rows.map(n=>t>=0?String(n[t]):"—")),r=u.rows.filter(n=>isFinite(Number(n[e]))).length,a=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),r<a&&warn("radar-missing-value",`有 ${a-r} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(a,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(a,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(a,s);if("sankey"===l){needColumn(m.source,"encoding.source",!0),needColumn(m.target,"encoding.target",!0);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(a,s)}if("violin"===l||"beeswarm"===l){const n=toArray(m.y);return n.length?(needNumeric(n[0],"encoding.y",!0),"violin"===l&&n.length>1&&warn("violin-single-y",`violin 的 y 只认第一列「${n[0]}」:多组对比请用 encoding.series 拆系列,或改用 beeswarm。`,"encoding.y")):fail("missing-encoding-channel",`encoding.y 是必填的(观测值那一列)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),needColumn(m.x,"encoding.x",!1),finish(a,s)}if("calendar"===l){const n=needColumn(m.x,"encoding.x",!0);if(n>=0){const e=u.rows.filter(e=>!function isCalendarDate(n){return null!=n&&""!==n&&(n instanceof Date?!Number.isNaN(n.getTime()):isFinite(Date.parse(String(n))))}(e[n])).length;e&&warn("calendar-unparseable-date",`${e} 行的日期读不出来(建议写成 "YYYY-MM-DD"),这些格子会被跳过。`,"encoding.x")}return needNumeric(toArray(m.y)[0],"encoding.y",!0),finish(a,s)}if("alluvial"===l){const n=m.axes;return null==n?fail("missing-encoding-channel",`encoding.axes 是必填的(按顺序列出每个轴的列名,至少两个)。可用列:${u.columns.join(" / ")}。`,"encoding.axes"):Array.isArray(n)?n.length<2?fail("alluvial-axes-too-few",`多轴分类流至少要 2 个轴(相邻轴之间才有流量带),encoding.axes 现在只有 ${n.length} 个。可用列:${u.columns.join(" / ")}。`,"encoding.axes"):n.forEach((n,e)=>{const t=`encoding.axes[${e}]`,i=needColumn(n,t,!0);if(i<0)return;const o=new Set(u.rows.map(n=>toLabel(n[i])).filter(n=>""!==n));o.size<2&&warn("alluvial-single-category",`轴「${n}」只有 ${o.size} 个类目,画出来是一条直线、没有流量。`,t);const r=u.rows.filter(n=>{const e=n[i];return null==e||""===e}).length;r&&warn("alluvial-missing-axis-value",`${r} 行在轴「${n}」上是空的,这些行会被跳过。`,t)}):fail("invalid-axes",`encoding.axes 要是列名数组(按顺序),例如 ["${u.columns[0]}", "${u.columns[1]||u.columns[0]}"]。`,"encoding.axes"),m.value&&needNumeric(m.value,"encoding.value",!1),finish(a,s)}if("line"===l||"area"===l||"bar"===l||"scatter"===l||"hexbin"===l){"scatter"===l&&m.size&&needNumeric(m.size,"encoding.size",!1),"hexbin"===l&&needNumeric(m.x,"encoding.x",!0);const n=toArray(m.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(m.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return m.series&&needColumn(m.series,"encoding.series",!1),finish(a,s)}return warn("passthrough-kind",`kind「${l}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(a,s)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const r=o.includes(e.kind);r||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${o.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,o)=>{if(r&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${o} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,o);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${o} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,o)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${o} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,o)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const r="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,r,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,r,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.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:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),r=columnIndex(e,i.value),a=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[r])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?a.slice(0,1):a}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.series),a=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;a.includes(e)||a.push(e);const c=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=a.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:a.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:a.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.value||t.color),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);e&&t&&null!==l&&(a.includes(e)||a.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:a},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),r=columnIndex(e,t.total),a=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),a=r>=0?n[r]:void 0;return null!=a&&""!==a&&!1!==a&&0!==Number(a)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.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:a}]}}(n,e);case"violin":return function compileViolin(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=t.series?columnIndex(e,t.series):-1,a=[];for(const n of e.rows){const e=i>=0?toLabel(n[i]):"全部";e&&!a.includes(e)&&a.push(e)}const collect=n=>{const e=new Map;for(const t of n){const n=i>=0?toLabel(t[i]):"全部";if(!n)continue;const r=toNumber(t[o]);null!==r&&(e.has(n)||e.set(n,[]),e.get(n).push(r))}return a.map(n=>e.get(n)||[])};let s;if(r>=0){const n=new Map;for(const t of e.rows){const e=toLabel(t[r])||"(空)";n.has(e)||n.set(e,[]),n.get(e).push(t)}s=[...n.entries()].map(([n,e],t)=>({id:`series-${t+1}`,type:"violin",name:n,data:collect(e)}))}else s=[{id:"series-1",type:"violin",name:firstColumn(t.y),data:collect(e.rows)}];return{legend:{show:s.length>1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:a},yAxis:{name:firstColumn(t.y)},series:s}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),r=columnIndex(e,t.value),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);!e||!t||null===l||l<=0||(a.some(n=>n.name===e)||a.push({name:e}),a.some(n=>n.name===t)||a.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:a,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);case"calendar":return function compileCalendar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=e.rows.map(n=>({date:n[i],value:toNumber(n[o])})).filter(n=>null!==n.date&&void 0!==n.date&&""!==n.date&&null!==n.value);return{legend:{show:!1},tooltip:{trigger:"item"},...n.calendar?{calendar:n.calendar}:{},series:[{id:"calendar",type:"calendar",name:firstColumn(t.y),data:r}]}}(n,e);case"alluvial":return function compileAlluvial(n,e){const t=n.encoding||{},i=(Array.isArray(t.axes)?t.axes:[]).filter(n=>"string"==typeof n&&n),o=e.rows.map(n=>{const t={};return e.columns.forEach((e,i)=>{t[e]=n[i]}),t});return{legend:{show:!1},tooltip:{trigger:"item"},alluvial:{axes:i,rows:o,...t.value?{valueField:t.value}:{},...n.alluvial||{}},series:[{id:"alluvial",type:"alluvial",name:t.value}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),r=toArray(i.y),a=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,c="scatter"===t||"beeswarm"===t||"hexbin"===t,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let m;if(l>=0){const n=new Map,i=columnIndex(e,r[0]);for(const t of e.rows){const e=a?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const r=toLabel(t[l])||"(空)";n.has(r)||n.set(r,new Map),n.get(r).set(e,toNumber(t[i]))}m=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else m=r.map((n,i)=>{const r=columnIndex(e,n),l=e.rows.map(n=>{const e=a?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[r]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return a||c?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const d={legend:{show:m.length>1},tooltip:{trigger:c?"item":"axis"},series:m};d.crosshair="scatter"===t||"hexbin"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return d.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},d.yAxis={name:1===r.length?r[0]:void 0},d}(n,e,t)}}(n,e,t))}if(n.matrix&&function isCartesianKind(n){return"line"===n||"area"===n||"bar"===n||"scatter"===n||"violin"===n||"beeswarm"===n||"hexbin"===n}(t)){i.matrix=n.matrix;const e=Array.isArray(i.series)?i.series:[];e.length>1&&e.forEach((n,e)=>{n&&"object"==typeof n&&(n.panel=e)})}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}exports.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","waterfall","heatmap","violin","beeswarm","hexbin","calendar","alluvial","sankey","pie","radar","funnel","gauge","liquid","function"],exports.CHART_DSL_KINDS=e,exports.CHART_DSL_SCHEMA_VERSION=1,exports.ChartDslCompileError=ChartDslCompileError,exports.chartDslToJsonString=function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))},exports.columnIndex=columnIndex,exports.compileChartDsl=compileChartDsl,exports.formatDiagnostics=function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"},exports.numericRatio=numericRatio,exports.renderChartDsl=function renderChartDsl(e,t,i){const o=validateChartDsl(t),r=compileChartDsl(t);return{chart:n.createChart(e,r,i),option:r,diagnostics:o}},exports.resolveDataset=resolveDataset,exports.validateChartDsl=validateChartDsl;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{diagnoseExpression as n,evaluateExpression as e,createChart as t}from"@damoqiongqiu/ice-chart";const i=1,o=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","violin","beeswarm","waterfall","funnel","graph","parametric","liquid"],r=["line","area","bar","scatter","waterfall","heatmap","violin","beeswarm","sankey","pie","radar","funnel","gauge","liquid","function"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function unique(n){const e=[];for(const t of n)e.some(n=>n===t)||e.push(t);return e}function firstColumn(n){return Array.isArray(n)?n[0]:n}const a=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series","matrix"],s=["x","y","series","size","color","name","value","total","source","target"],l=["line","area","bar","scatter","violin","beeswarm"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(t){const i=[],r=[],fail=(n,e,t)=>i.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>r.push({severity:"warning",code:n,message:e,path:t});if(!t||"object"!=typeof t||Array.isArray(t))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(i,r);void 0!==t.schemaVersion&&1!==t.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${t.schemaVersion}(当前是 1)。`,"schemaVersion"),t.kind?o.includes(t.kind)||fail("unsupported-kind",`不支持的 kind「${t.kind}」。可用类型:${o.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${o.join(" / ")}。`,"kind");for(const n of Object.keys(t))a.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);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 o=String(t.expression||"");if(o.trim()){const a="function"===l?"x":"t",s=t.params&&"object"==typeof t.params?t.params:{},c=function sampleExpression(n,t,i,o,r){const a="function"===r?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):a[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):a[1];if(!(l>s))return[];const c=[],u=64;try{for(let o=0;o<=u;o++){const r=s+(l-s)*o/u,a={...i,[t]:r},m=e(n,a);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(o,a,s,t.domain,l);for(const e of n(o,{variable:a,params:s,values:c})){const n={severity:e.severity,code:`expression-${e.code}`,message:e.message,path:"expression"};("error"===e.severity?i:r).push(n)}}else fail("missing-expression",`kind「${l}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(i,r)}if(c)return validateAnnotation(t.annotation,{kind:l,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(i,r);const u=resolveDataset(t);if(void 0===t.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(i,r);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(i,r);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(i,r);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(i,r);u.rows.forEach((n,e)=>{n.length!==u.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${e}]`)});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 n of Object.keys(m))s.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const d=m.x?columnIndex(u,m.x):-1;validateAnnotation(t.annotation,{kind:l,dataset:u,xIndex:d,numericX:d>=0&&numericRatio(u,d)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${u.columns.join(" / ")}。`,e),-1;const i=columnIndex(u,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${u.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(u,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(u,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if(function validateMatrix(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-matrix","matrix 需要 { rows, columns, gap? }:rows / columns 是数字(等分)或权重数组。","matrix");const o=n,validSize=n=>"number"==typeof n&&isFinite(n)&&n>0||Array.isArray(n)&&n.length>0&&n.every(n=>"number"==typeof n&&isFinite(n)&&n>0);validSize(o.rows)||t("invalid-matrix","matrix.rows 要是正数(等分几行)或正数数组(每行的权重)。","matrix.rows");validSize(o.columns)||t("invalid-matrix","matrix.columns 要是正数(等分几列)或正数数组(每列的权重)。","matrix.columns");void 0!==o.gap&&("number"!=typeof o.gap||!isFinite(o.gap)||o.gap<0)&&t("invalid-matrix","matrix.gap 要是非负数字(面板间距,设备像素)。","matrix.gap");if(!e.cartesian)return void i("matrix-ignored",`kind「${e.kind}」不是直角坐标场景,matrix 会被忽略(面板矩阵只在直角坐标下生效)。`,"matrix");const r=("number"==typeof o.rows?Math.floor(o.rows):Array.isArray(o.rows)?o.rows.length:0)*("number"==typeof o.columns?Math.floor(o.columns):Array.isArray(o.columns)?o.columns.length:0);e.seriesCount<=1?i("matrix-single-panel","matrix 只拆出一块面板:分面靠 encoding.series(每个分组一块面板)或多个 y 列,至少要两个。","matrix"):r&&e.seriesCount>r&&i("matrix-too-few-panels",`${e.seriesCount} 个分组只有 ${r} 块面板,多出来的会挤在最后一块上。`,"matrix")}(t.matrix,{kind:l,cartesian:"line"===l||"area"===l||"bar"===l||"scatter"===l||"violin"===l||"beeswarm"===l,seriesCount:m.series&&columnIndex(u,m.series)>=0?unique(u.rows.map(n=>toLabel(n[columnIndex(u,m.series)])).filter(n=>""!==n)).length:Math.max(1,toArray(m.y).length)},fail,warn),"pie"===l||"funnel"===l||"gauge"===l||"liquid"===l){needColumn(m.name,"encoding.name","pie"===l||"funnel"===l);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0&&"pie"===l){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===l||"liquid"===l)&&u.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(i,r)}if("radar"===l){const n=needColumn(m.x,"encoding.x",!0),e=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"),n>=0&&e>=0){const i=new Set(u.rows.map(e=>String(e[n]))),o=new Set(u.rows.map(n=>t>=0?String(n[t]):"—")),r=u.rows.filter(n=>isFinite(Number(n[e]))).length,a=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),r<a&&warn("radar-missing-value",`有 ${a-r} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(i,r)}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,r);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,r);if("sankey"===l){needColumn(m.source,"encoding.source",!0),needColumn(m.target,"encoding.target",!0);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(i,r)}if("violin"===l||"beeswarm"===l){const n=toArray(m.y);return n.length?(needNumeric(n[0],"encoding.y",!0),"violin"===l&&n.length>1&&warn("violin-single-y",`violin 的 y 只认第一列「${n[0]}」:多组对比请用 encoding.series 拆系列,或改用 beeswarm。`,"encoding.y")):fail("missing-encoding-channel",`encoding.y 是必填的(观测值那一列)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),needColumn(m.x,"encoding.x",!1),finish(i,r)}if("line"===l||"area"===l||"bar"===l||"scatter"===l){"scatter"===l&&m.size&&needNumeric(m.size,"encoding.size",!1);const n=toArray(m.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(m.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return m.series&&needColumn(m.series,"encoding.series",!1),finish(i,r)}return warn("passthrough-kind",`kind「${l}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(i,r)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const o=l.includes(e.kind);o||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${l.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,r)=>{if(o&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${r} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,r);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${r} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,r)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${r} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,r)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const r="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,r,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,r,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.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:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),r=columnIndex(e,i.value),a=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[r])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?a.slice(0,1):a}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.series),a=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;a.includes(e)||a.push(e);const c=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=a.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:a.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:a.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.value||t.color),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);e&&t&&null!==l&&(a.includes(e)||a.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:a},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),r=columnIndex(e,t.total),a=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),a=r>=0?n[r]:void 0;return null!=a&&""!==a&&!1!==a&&0!==Number(a)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.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:a}]}}(n,e);case"violin":return function compileViolin(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=t.series?columnIndex(e,t.series):-1,a=[];for(const n of e.rows){const e=i>=0?toLabel(n[i]):"全部";e&&!a.includes(e)&&a.push(e)}const collect=n=>{const e=new Map;for(const t of n){const n=i>=0?toLabel(t[i]):"全部";if(!n)continue;const r=toNumber(t[o]);null!==r&&(e.has(n)||e.set(n,[]),e.get(n).push(r))}return a.map(n=>e.get(n)||[])};let s;if(r>=0){const n=new Map;for(const t of e.rows){const e=toLabel(t[r])||"(空)";n.has(e)||n.set(e,[]),n.get(e).push(t)}s=[...n.entries()].map(([n,e],t)=>({id:`series-${t+1}`,type:"violin",name:n,data:collect(e)}))}else s=[{id:"series-1",type:"violin",name:firstColumn(t.y),data:collect(e.rows)}];return{legend:{show:s.length>1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:a},yAxis:{name:firstColumn(t.y)},series:s}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),r=columnIndex(e,t.value),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);!e||!t||null===l||l<=0||(a.some(n=>n.name===e)||a.push({name:e}),a.some(n=>n.name===t)||a.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:a,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),r=toArray(i.y),a=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,c="scatter"===t||"beeswarm"===t,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let m;if(l>=0){const n=new Map,i=columnIndex(e,r[0]);for(const t of e.rows){const e=a?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const r=toLabel(t[l])||"(空)";n.has(r)||n.set(r,new Map),n.get(r).set(e,toNumber(t[i]))}m=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else m=r.map((n,i)=>{const r=columnIndex(e,n),l=e.rows.map(n=>{const e=a?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[r]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return a||c?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const d={legend:{show:m.length>1},tooltip:{trigger:c?"item":"axis"},series:m};d.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return d.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},d.yAxis={name:1===r.length?r[0]:void 0},d}(n,e,t)}}(n,e,t))}if(n.matrix&&function isCartesianKind(n){return"line"===n||"area"===n||"bar"===n||"scatter"===n||"violin"===n||"beeswarm"===n}(t)){i.matrix=n.matrix;const e=Array.isArray(i.series)?i.series:[];e.length>1&&e.forEach((n,e)=>{n&&"object"==typeof n&&(n.panel=e)})}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))}function renderChartDsl(n,e,i){const o=validateChartDsl(e),r=compileChartDsl(e);return{chart:t(n,r,i),option:r,diagnostics:o}}export{r as CHART_DSL_COMPILED_KINDS,o as CHART_DSL_KINDS,i as CHART_DSL_SCHEMA_VERSION,ChartDslCompileError,chartDslToJsonString,columnIndex,compileChartDsl,formatDiagnostics,numericRatio,renderChartDsl,resolveDataset,validateChartDsl};
1
+ import{diagnoseExpression as n,evaluateExpression as e,createChart as t}from"@damoqiongqiu/ice-chart";const i=1,o=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","violin","beeswarm","hexbin","calendar","alluvial","waterfall","funnel","graph","parametric","liquid"],a=["line","area","bar","scatter","waterfall","heatmap","violin","beeswarm","hexbin","calendar","alluvial","sankey","pie","radar","funnel","gauge","liquid","function"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function unique(n){const e=[];for(const t of n)e.some(n=>n===t)||e.push(t);return e}function firstColumn(n){return Array.isArray(n)?n[0]:n}const r=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series","matrix","calendar","alluvial"],s=["x","y","series","size","color","name","value","total","source","target","axes"],l=["line","area","bar","scatter","violin","beeswarm","hexbin"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(t){const i=[],a=[],fail=(n,e,t)=>i.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>a.push({severity:"warning",code:n,message:e,path:t});if(!t||"object"!=typeof t||Array.isArray(t))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(i,a);void 0!==t.schemaVersion&&1!==t.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${t.schemaVersion}(当前是 1)。`,"schemaVersion"),t.kind?o.includes(t.kind)||fail("unsupported-kind",`不支持的 kind「${t.kind}」。可用类型:${o.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${o.join(" / ")}。`,"kind");for(const n of Object.keys(t))r.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const c=t.kind,u=Array.isArray(t.series)&&t.series.length>0;if(u&&t.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===c||"parametric"===c){const o=String(t.expression||"");if(o.trim()){const r="function"===c?"x":"t",s=t.params&&"object"==typeof t.params?t.params:{},l=function sampleExpression(n,t,i,o,a){const r="function"===a?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):r[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):r[1];if(!(l>s))return[];const c=[],u=64;try{for(let o=0;o<=u;o++){const a=s+(l-s)*o/u,r={...i,[t]:a},m=e(n,r);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(o,r,s,t.domain,c);for(const e of n(o,{variable:r,params:s,values:l})){const n={severity:e.severity,code:`expression-${e.code}`,message:e.message,path:"expression"};("error"===e.severity?i:a).push(n)}}else fail("missing-expression",`kind「${c}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(i,a)}if(u)return validateAnnotation(t.annotation,{kind:c,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(i,a);const m=resolveDataset(t);if(void 0===t.data)return fail("missing-data",`kind「${c}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(i,a);if(!m)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(i,a);if(!m.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(i,a);if(!m.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(i,a);m.rows.forEach((n,e)=>{n.length!==m.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${m.columns.length}(columns:${m.columns.join(" / ")})。`,`data.rows[${e}]`)});const d=t.encoding&&"object"==typeof t.encoding?t.encoding:{};t.encoding||fail("missing-encoding",`kind「${c}」需要 encoding 把列绑到通道上,例如 { "x": "${m.columns[0]}", "y": "${m.columns[1]||m.columns[0]}" }。`,"encoding");for(const n of Object.keys(d))s.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const g=d.x?columnIndex(m,d.x):-1;validateAnnotation(t.annotation,{kind:c,dataset:m,xIndex:g,numericX:g>=0&&numericRatio(m,g)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${m.columns.join(" / ")}。`,e),-1;const i=columnIndex(m,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${m.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(m,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(m,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if(function validateMatrix(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-matrix","matrix 需要 { rows, columns, gap? }:rows / columns 是数字(等分)或权重数组。","matrix");const o=n,validSize=n=>"number"==typeof n&&isFinite(n)&&n>0||Array.isArray(n)&&n.length>0&&n.every(n=>"number"==typeof n&&isFinite(n)&&n>0);validSize(o.rows)||t("invalid-matrix","matrix.rows 要是正数(等分几行)或正数数组(每行的权重)。","matrix.rows");validSize(o.columns)||t("invalid-matrix","matrix.columns 要是正数(等分几列)或正数数组(每列的权重)。","matrix.columns");void 0!==o.gap&&("number"!=typeof o.gap||!isFinite(o.gap)||o.gap<0)&&t("invalid-matrix","matrix.gap 要是非负数字(面板间距,设备像素)。","matrix.gap");if(!e.cartesian)return void i("matrix-ignored",`kind「${e.kind}」不是直角坐标场景,matrix 会被忽略(面板矩阵只在直角坐标下生效)。`,"matrix");const a=("number"==typeof o.rows?Math.floor(o.rows):Array.isArray(o.rows)?o.rows.length:0)*("number"==typeof o.columns?Math.floor(o.columns):Array.isArray(o.columns)?o.columns.length:0);e.seriesCount<=1?i("matrix-single-panel","matrix 只拆出一块面板:分面靠 encoding.series(每个分组一块面板)或多个 y 列,至少要两个。","matrix"):a&&e.seriesCount>a&&i("matrix-too-few-panels",`${e.seriesCount} 个分组只有 ${a} 块面板,多出来的会挤在最后一块上。`,"matrix")}(t.matrix,{kind:c,cartesian:l.includes(c),seriesCount:d.series&&columnIndex(m,d.series)>=0?unique(m.rows.map(n=>toLabel(n[columnIndex(m,d.series)])).filter(n=>""!==n)).length:Math.max(1,toArray(d.y).length)},fail,warn),"pie"===c||"funnel"===c||"gauge"===c||"liquid"===c){needColumn(d.name,"encoding.name","pie"===c||"funnel"===c);const n=needNumeric(d.value,"encoding.value",!0);if(n>=0&&"pie"===c){const e=m.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===c||"liquid"===c)&&m.rows.length>1&&warn("single-value-kind",`kind「${c}」只画一个值,${m.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(i,a)}if("radar"===c){const n=needColumn(d.x,"encoding.x",!0),e=needNumeric(toArray(d.y)[0],"encoding.y",!0),t=d.series?needColumn(d.series,"encoding.series",!1):-1;if(toArray(d.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),n>=0&&e>=0){const i=new Set(m.rows.map(e=>String(e[n]))),o=new Set(m.rows.map(n=>t>=0?String(n[t]):"—")),a=m.rows.filter(n=>isFinite(Number(n[e]))).length,r=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),a<r&&warn("radar-missing-value",`有 ${r-a} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(i,a)}if("heatmap"===c)return needColumn(d.x,"encoding.x",!0),needColumn(d.y,"encoding.y",!0),needNumeric(d.value||d.color,d.value?"encoding.value":"encoding.color",!0),finish(i,a);if("waterfall"===c)return needColumn(d.name,"encoding.name",!0),needNumeric(d.value,"encoding.value",!0),d.total&&needColumn(d.total,"encoding.total",!1),finish(i,a);if("sankey"===c){needColumn(d.source,"encoding.source",!0),needColumn(d.target,"encoding.target",!0);const n=needNumeric(d.value,"encoding.value",!0);if(n>=0){const e=m.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(i,a)}if("violin"===c||"beeswarm"===c){const n=toArray(d.y);return n.length?(needNumeric(n[0],"encoding.y",!0),"violin"===c&&n.length>1&&warn("violin-single-y",`violin 的 y 只认第一列「${n[0]}」:多组对比请用 encoding.series 拆系列,或改用 beeswarm。`,"encoding.y")):fail("missing-encoding-channel",`encoding.y 是必填的(观测值那一列)。可用列:${m.columns.join(" / ")}。`,"encoding.y"),needColumn(d.x,"encoding.x",!1),finish(i,a)}if("calendar"===c){const n=needColumn(d.x,"encoding.x",!0);if(n>=0){const e=m.rows.filter(e=>!function isCalendarDate(n){return null!=n&&""!==n&&(n instanceof Date?!Number.isNaN(n.getTime()):isFinite(Date.parse(String(n))))}(e[n])).length;e&&warn("calendar-unparseable-date",`${e} 行的日期读不出来(建议写成 "YYYY-MM-DD"),这些格子会被跳过。`,"encoding.x")}return needNumeric(toArray(d.y)[0],"encoding.y",!0),finish(i,a)}if("alluvial"===c){const n=d.axes;return null==n?fail("missing-encoding-channel",`encoding.axes 是必填的(按顺序列出每个轴的列名,至少两个)。可用列:${m.columns.join(" / ")}。`,"encoding.axes"):Array.isArray(n)?n.length<2?fail("alluvial-axes-too-few",`多轴分类流至少要 2 个轴(相邻轴之间才有流量带),encoding.axes 现在只有 ${n.length} 个。可用列:${m.columns.join(" / ")}。`,"encoding.axes"):n.forEach((n,e)=>{const t=`encoding.axes[${e}]`,i=needColumn(n,t,!0);if(i<0)return;const o=new Set(m.rows.map(n=>toLabel(n[i])).filter(n=>""!==n));o.size<2&&warn("alluvial-single-category",`轴「${n}」只有 ${o.size} 个类目,画出来是一条直线、没有流量。`,t);const a=m.rows.filter(n=>{const e=n[i];return null==e||""===e}).length;a&&warn("alluvial-missing-axis-value",`${a} 行在轴「${n}」上是空的,这些行会被跳过。`,t)}):fail("invalid-axes",`encoding.axes 要是列名数组(按顺序),例如 ["${m.columns[0]}", "${m.columns[1]||m.columns[0]}"]。`,"encoding.axes"),d.value&&needNumeric(d.value,"encoding.value",!1),finish(i,a)}if("line"===c||"area"===c||"bar"===c||"scatter"===c||"hexbin"===c){"scatter"===c&&d.size&&needNumeric(d.size,"encoding.size",!1),"hexbin"===c&&needNumeric(d.x,"encoding.x",!0);const n=toArray(d.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${m.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(d.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return d.series&&needColumn(d.series,"encoding.series",!1),finish(i,a)}return warn("passthrough-kind",`kind「${c}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(i,a)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const o=l.includes(e.kind);o||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${l.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,a)=>{if(o&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${a} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,a);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${a} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,a)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${a} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,a)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const a="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,a,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,a,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.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:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),a=columnIndex(e,i.value),r=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[a])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?r.slice(0,1):r}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.series),r=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;r.includes(e)||r.push(e);const c=a>=0?toLabel(n[a])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=r.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:r.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:r.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.value||t.color),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);e&&t&&null!==l&&(r.includes(e)||r.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:r},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),a=columnIndex(e,t.total),r=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),r=a>=0?n[a]:void 0;return null!=r&&""!==r&&!1!==r&&0!==Number(r)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.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:r}]}}(n,e);case"violin":return function compileViolin(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=t.series?columnIndex(e,t.series):-1,r=[];for(const n of e.rows){const e=i>=0?toLabel(n[i]):"全部";e&&!r.includes(e)&&r.push(e)}const collect=n=>{const e=new Map;for(const t of n){const n=i>=0?toLabel(t[i]):"全部";if(!n)continue;const a=toNumber(t[o]);null!==a&&(e.has(n)||e.set(n,[]),e.get(n).push(a))}return r.map(n=>e.get(n)||[])};let s;if(a>=0){const n=new Map;for(const t of e.rows){const e=toLabel(t[a])||"(空)";n.has(e)||n.set(e,[]),n.get(e).push(t)}s=[...n.entries()].map(([n,e],t)=>({id:`series-${t+1}`,type:"violin",name:n,data:collect(e)}))}else s=[{id:"series-1",type:"violin",name:firstColumn(t.y),data:collect(e.rows)}];return{legend:{show:s.length>1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:r},yAxis:{name:firstColumn(t.y)},series:s}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),a=columnIndex(e,t.value),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);!e||!t||null===l||l<=0||(r.some(n=>n.name===e)||r.push({name:e}),r.some(n=>n.name===t)||r.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:r,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);case"calendar":return function compileCalendar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=e.rows.map(n=>({date:n[i],value:toNumber(n[o])})).filter(n=>null!==n.date&&void 0!==n.date&&""!==n.date&&null!==n.value);return{legend:{show:!1},tooltip:{trigger:"item"},...n.calendar?{calendar:n.calendar}:{},series:[{id:"calendar",type:"calendar",name:firstColumn(t.y),data:a}]}}(n,e);case"alluvial":return function compileAlluvial(n,e){const t=n.encoding||{},i=(Array.isArray(t.axes)?t.axes:[]).filter(n=>"string"==typeof n&&n),o=e.rows.map(n=>{const t={};return e.columns.forEach((e,i)=>{t[e]=n[i]}),t});return{legend:{show:!1},tooltip:{trigger:"item"},alluvial:{axes:i,rows:o,...t.value?{valueField:t.value}:{},...n.alluvial||{}},series:[{id:"alluvial",type:"alluvial",name:t.value}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),a=toArray(i.y),r=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,c="scatter"===t||"beeswarm"===t||"hexbin"===t,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let m;if(l>=0){const n=new Map,i=columnIndex(e,a[0]);for(const t of e.rows){const e=r?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const a=toLabel(t[l])||"(空)";n.has(a)||n.set(a,new Map),n.get(a).set(e,toNumber(t[i]))}m=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>r?[n,e.get(n)??null]:e.get(n)??null)}))}else m=a.map((n,i)=>{const a=columnIndex(e,n),l=e.rows.map(n=>{const e=r?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[a]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return r||c?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const d={legend:{show:m.length>1},tooltip:{trigger:c?"item":"axis"},series:m};d.crosshair="scatter"===t||"hexbin"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return d.xAxis=r?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},d.yAxis={name:1===a.length?a[0]:void 0},d}(n,e,t)}}(n,e,t))}if(n.matrix&&function isCartesianKind(n){return"line"===n||"area"===n||"bar"===n||"scatter"===n||"violin"===n||"beeswarm"===n||"hexbin"===n}(t)){i.matrix=n.matrix;const e=Array.isArray(i.series)?i.series:[];e.length>1&&e.forEach((n,e)=>{n&&"object"==typeof n&&(n.panel=e)})}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))}function renderChartDsl(n,e,i){const o=validateChartDsl(e),a=compileChartDsl(e);return{chart:t(n,a,i),option:a,diagnostics:o}}export{a as CHART_DSL_COMPILED_KINDS,o 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(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@damoqiongqiu/ice-chart")):"function"==typeof define&&define.amd?define(["exports","@damoqiongqiu/ice-chart"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).ICEChartDSL={},n.ICEChart)}(this,function(n,e){"use strict";const t=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","violin","beeswarm","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function unique(n){const e=[];for(const t of n)e.some(n=>n===t)||e.push(t);return e}function firstColumn(n){return Array.isArray(n)?n[0]:n}const i=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series","matrix"],o=["x","y","series","size","color","name","value","total","source","target"],r=["line","area","bar","scatter","violin","beeswarm"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(n){const r=[],a=[],fail=(n,e,t)=>r.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>a.push({severity:"warning",code:n,message:e,path:t});if(!n||"object"!=typeof n||Array.isArray(n))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(r,a);void 0!==n.schemaVersion&&1!==n.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${n.schemaVersion}(当前是 1)。`,"schemaVersion"),n.kind?t.includes(n.kind)||fail("unsupported-kind",`不支持的 kind「${n.kind}」。可用类型:${t.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${t.join(" / ")}。`,"kind");for(const e of Object.keys(n))i.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const s=n.kind,l=Array.isArray(n.series)&&n.series.length>0;if(l&&n.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===s||"parametric"===s){const t=String(n.expression||"");if(t.trim()){const i="function"===s?"x":"t",o=n.params&&"object"==typeof n.params?n.params:{},l=function sampleExpression(n,t,i,o,r){const a="function"===r?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):a[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):a[1];if(!(l>s))return[];const c=[],u=64;try{for(let o=0;o<=u;o++){const r=s+(l-s)*o/u,a={...i,[t]:r},m=e.evaluateExpression(n,a);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(t,i,o,n.domain,s);for(const n of e.diagnoseExpression(t,{variable:i,params:o,values:l})){const e={severity:n.severity,code:`expression-${n.code}`,message:n.message,path:"expression"};("error"===n.severity?r:a).push(e)}}else fail("missing-expression",`kind「${s}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(r,a)}if(l)return validateAnnotation(n.annotation,{kind:s,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(r,a);const c=resolveDataset(n);if(void 0===n.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(r,a);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(r,a);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(r,a);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(r,a);c.rows.forEach((n,e)=>{n.length!==c.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${e}]`)});const u=n.encoding&&"object"==typeof n.encoding?n.encoding:{};n.encoding||fail("missing-encoding",`kind「${s}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.columns[0]}" }。`,"encoding");for(const n of Object.keys(u))o.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const m=u.x?columnIndex(c,u.x):-1;validateAnnotation(n.annotation,{kind:s,dataset:c,xIndex:m,numericX:m>=0&&numericRatio(c,m)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${c.columns.join(" / ")}。`,e),-1;const i=columnIndex(c,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${c.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(c,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(c,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if(function validateMatrix(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-matrix","matrix 需要 { rows, columns, gap? }:rows / columns 是数字(等分)或权重数组。","matrix");const o=n,validSize=n=>"number"==typeof n&&isFinite(n)&&n>0||Array.isArray(n)&&n.length>0&&n.every(n=>"number"==typeof n&&isFinite(n)&&n>0);validSize(o.rows)||t("invalid-matrix","matrix.rows 要是正数(等分几行)或正数数组(每行的权重)。","matrix.rows");validSize(o.columns)||t("invalid-matrix","matrix.columns 要是正数(等分几列)或正数数组(每列的权重)。","matrix.columns");void 0!==o.gap&&("number"!=typeof o.gap||!isFinite(o.gap)||o.gap<0)&&t("invalid-matrix","matrix.gap 要是非负数字(面板间距,设备像素)。","matrix.gap");if(!e.cartesian)return void i("matrix-ignored",`kind「${e.kind}」不是直角坐标场景,matrix 会被忽略(面板矩阵只在直角坐标下生效)。`,"matrix");const r=("number"==typeof o.rows?Math.floor(o.rows):Array.isArray(o.rows)?o.rows.length:0)*("number"==typeof o.columns?Math.floor(o.columns):Array.isArray(o.columns)?o.columns.length:0);e.seriesCount<=1?i("matrix-single-panel","matrix 只拆出一块面板:分面靠 encoding.series(每个分组一块面板)或多个 y 列,至少要两个。","matrix"):r&&e.seriesCount>r&&i("matrix-too-few-panels",`${e.seriesCount} 个分组只有 ${r} 块面板,多出来的会挤在最后一块上。`,"matrix")}(n.matrix,{kind:s,cartesian:"line"===s||"area"===s||"bar"===s||"scatter"===s||"violin"===s||"beeswarm"===s,seriesCount:u.series&&columnIndex(c,u.series)>=0?unique(c.rows.map(n=>toLabel(n[columnIndex(c,u.series)])).filter(n=>""!==n)).length:Math.max(1,toArray(u.y).length)},fail,warn),"pie"===s||"funnel"===s||"gauge"===s||"liquid"===s){needColumn(u.name,"encoding.name","pie"===s||"funnel"===s);const n=needNumeric(u.value,"encoding.value",!0);if(n>=0&&"pie"===s){const e=c.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===s||"liquid"===s)&&c.rows.length>1&&warn("single-value-kind",`kind「${s}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(r,a)}if("radar"===s){const n=needColumn(u.x,"encoding.x",!0),e=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"),n>=0&&e>=0){const i=new Set(c.rows.map(e=>String(e[n]))),o=new Set(c.rows.map(n=>t>=0?String(n[t]):"—")),r=c.rows.filter(n=>isFinite(Number(n[e]))).length,a=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),r<a&&warn("radar-missing-value",`有 ${a-r} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(r,a)}if("heatmap"===s)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(r,a);if("waterfall"===s)return needColumn(u.name,"encoding.name",!0),needNumeric(u.value,"encoding.value",!0),u.total&&needColumn(u.total,"encoding.total",!1),finish(r,a);if("sankey"===s){needColumn(u.source,"encoding.source",!0),needColumn(u.target,"encoding.target",!0);const n=needNumeric(u.value,"encoding.value",!0);if(n>=0){const e=c.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(r,a)}if("violin"===s||"beeswarm"===s){const n=toArray(u.y);return n.length?(needNumeric(n[0],"encoding.y",!0),"violin"===s&&n.length>1&&warn("violin-single-y",`violin 的 y 只认第一列「${n[0]}」:多组对比请用 encoding.series 拆系列,或改用 beeswarm。`,"encoding.y")):fail("missing-encoding-channel",`encoding.y 是必填的(观测值那一列)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),needColumn(u.x,"encoding.x",!1),finish(r,a)}if("line"===s||"area"===s||"bar"===s||"scatter"===s){"scatter"===s&&u.size&&needNumeric(u.size,"encoding.size",!1);const n=toArray(u.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(u.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return u.series&&needColumn(u.series,"encoding.series",!1),finish(r,a)}return warn("passthrough-kind",`kind「${s}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(r,a)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const o=r.includes(e.kind);o||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${r.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,r)=>{if(o&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${r} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,r);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${r} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,r)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${r} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,r)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const r="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,r,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,r,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.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:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),r=columnIndex(e,i.value),a=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[r])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?a.slice(0,1):a}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.series),a=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;a.includes(e)||a.push(e);const c=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=a.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:a.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:a.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.value||t.color),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);e&&t&&null!==l&&(a.includes(e)||a.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:a},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),r=columnIndex(e,t.total),a=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),a=r>=0?n[r]:void 0;return null!=a&&""!==a&&!1!==a&&0!==Number(a)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.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:a}]}}(n,e);case"violin":return function compileViolin(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=t.series?columnIndex(e,t.series):-1,a=[];for(const n of e.rows){const e=i>=0?toLabel(n[i]):"全部";e&&!a.includes(e)&&a.push(e)}const collect=n=>{const e=new Map;for(const t of n){const n=i>=0?toLabel(t[i]):"全部";if(!n)continue;const r=toNumber(t[o]);null!==r&&(e.has(n)||e.set(n,[]),e.get(n).push(r))}return a.map(n=>e.get(n)||[])};let s;if(r>=0){const n=new Map;for(const t of e.rows){const e=toLabel(t[r])||"(空)";n.has(e)||n.set(e,[]),n.get(e).push(t)}s=[...n.entries()].map(([n,e],t)=>({id:`series-${t+1}`,type:"violin",name:n,data:collect(e)}))}else s=[{id:"series-1",type:"violin",name:firstColumn(t.y),data:collect(e.rows)}];return{legend:{show:s.length>1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:a},yAxis:{name:firstColumn(t.y)},series:s}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),r=columnIndex(e,t.value),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);!e||!t||null===l||l<=0||(a.some(n=>n.name===e)||a.push({name:e}),a.some(n=>n.name===t)||a.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:a,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),r=toArray(i.y),a=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,c="scatter"===t||"beeswarm"===t,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let m;if(l>=0){const n=new Map,i=columnIndex(e,r[0]);for(const t of e.rows){const e=a?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const r=toLabel(t[l])||"(空)";n.has(r)||n.set(r,new Map),n.get(r).set(e,toNumber(t[i]))}m=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else m=r.map((n,i)=>{const r=columnIndex(e,n),l=e.rows.map(n=>{const e=a?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[r]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return a||c?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const d={legend:{show:m.length>1},tooltip:{trigger:c?"item":"axis"},series:m};d.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return d.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},d.yAxis={name:1===r.length?r[0]:void 0},d}(n,e,t)}}(n,e,t))}if(n.matrix&&function isCartesianKind(n){return"line"===n||"area"===n||"bar"===n||"scatter"===n||"violin"===n||"beeswarm"===n}(t)){i.matrix=n.matrix;const e=Array.isArray(i.series)?i.series:[];e.length>1&&e.forEach((n,e)=>{n&&"object"==typeof n&&(n.panel=e)})}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}n.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","waterfall","heatmap","violin","beeswarm","sankey","pie","radar","funnel","gauge","liquid","function"],n.CHART_DSL_KINDS=t,n.CHART_DSL_SCHEMA_VERSION=1,n.ChartDslCompileError=ChartDslCompileError,n.chartDslToJsonString=function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))},n.columnIndex=columnIndex,n.compileChartDsl=compileChartDsl,n.formatDiagnostics=function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"},n.numericRatio=numericRatio,n.renderChartDsl=function renderChartDsl(n,t,i){const o=validateChartDsl(t),r=compileChartDsl(t);return{chart:e.createChart(n,r,i),option:r,diagnostics:o}},n.resolveDataset=resolveDataset,n.validateChartDsl=validateChartDsl});
1
+ !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@damoqiongqiu/ice-chart")):"function"==typeof define&&define.amd?define(["exports","@damoqiongqiu/ice-chart"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).ICEChartDSL={},n.ICEChart)}(this,function(n,e){"use strict";const t=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","violin","beeswarm","hexbin","calendar","alluvial","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function unique(n){const e=[];for(const t of n)e.some(n=>n===t)||e.push(t);return e}function firstColumn(n){return Array.isArray(n)?n[0]:n}const i=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series","matrix","calendar","alluvial"],o=["x","y","series","size","color","name","value","total","source","target","axes"],a=["line","area","bar","scatter","violin","beeswarm","hexbin"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(n){const r=[],s=[],fail=(n,e,t)=>r.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>s.push({severity:"warning",code:n,message:e,path:t});if(!n||"object"!=typeof n||Array.isArray(n))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(r,s);void 0!==n.schemaVersion&&1!==n.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${n.schemaVersion}(当前是 1)。`,"schemaVersion"),n.kind?t.includes(n.kind)||fail("unsupported-kind",`不支持的 kind「${n.kind}」。可用类型:${t.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${t.join(" / ")}。`,"kind");for(const e of Object.keys(n))i.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const l=n.kind,c=Array.isArray(n.series)&&n.series.length>0;if(c&&n.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===l||"parametric"===l){const t=String(n.expression||"");if(t.trim()){const i="function"===l?"x":"t",o=n.params&&"object"==typeof n.params?n.params:{},a=function sampleExpression(n,t,i,o,a){const r="function"===a?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):r[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):r[1];if(!(l>s))return[];const c=[],u=64;try{for(let o=0;o<=u;o++){const a=s+(l-s)*o/u,r={...i,[t]:a},m=e.evaluateExpression(n,r);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(t,i,o,n.domain,l);for(const n of e.diagnoseExpression(t,{variable:i,params:o,values:a})){const e={severity:n.severity,code:`expression-${n.code}`,message:n.message,path:"expression"};("error"===n.severity?r:s).push(e)}}else fail("missing-expression",`kind「${l}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(r,s)}if(c)return validateAnnotation(n.annotation,{kind:l,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(r,s);const u=resolveDataset(n);if(void 0===n.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(r,s);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(r,s);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(r,s);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(r,s);u.rows.forEach((n,e)=>{n.length!==u.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${e}]`)});const m=n.encoding&&"object"==typeof n.encoding?n.encoding:{};n.encoding||fail("missing-encoding",`kind「${l}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const n of Object.keys(m))o.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const d=m.x?columnIndex(u,m.x):-1;validateAnnotation(n.annotation,{kind:l,dataset:u,xIndex:d,numericX:d>=0&&numericRatio(u,d)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${u.columns.join(" / ")}。`,e),-1;const i=columnIndex(u,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${u.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(u,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(u,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if(function validateMatrix(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-matrix","matrix 需要 { rows, columns, gap? }:rows / columns 是数字(等分)或权重数组。","matrix");const o=n,validSize=n=>"number"==typeof n&&isFinite(n)&&n>0||Array.isArray(n)&&n.length>0&&n.every(n=>"number"==typeof n&&isFinite(n)&&n>0);validSize(o.rows)||t("invalid-matrix","matrix.rows 要是正数(等分几行)或正数数组(每行的权重)。","matrix.rows");validSize(o.columns)||t("invalid-matrix","matrix.columns 要是正数(等分几列)或正数数组(每列的权重)。","matrix.columns");void 0!==o.gap&&("number"!=typeof o.gap||!isFinite(o.gap)||o.gap<0)&&t("invalid-matrix","matrix.gap 要是非负数字(面板间距,设备像素)。","matrix.gap");if(!e.cartesian)return void i("matrix-ignored",`kind「${e.kind}」不是直角坐标场景,matrix 会被忽略(面板矩阵只在直角坐标下生效)。`,"matrix");const a=("number"==typeof o.rows?Math.floor(o.rows):Array.isArray(o.rows)?o.rows.length:0)*("number"==typeof o.columns?Math.floor(o.columns):Array.isArray(o.columns)?o.columns.length:0);e.seriesCount<=1?i("matrix-single-panel","matrix 只拆出一块面板:分面靠 encoding.series(每个分组一块面板)或多个 y 列,至少要两个。","matrix"):a&&e.seriesCount>a&&i("matrix-too-few-panels",`${e.seriesCount} 个分组只有 ${a} 块面板,多出来的会挤在最后一块上。`,"matrix")}(n.matrix,{kind:l,cartesian:a.includes(l),seriesCount:m.series&&columnIndex(u,m.series)>=0?unique(u.rows.map(n=>toLabel(n[columnIndex(u,m.series)])).filter(n=>""!==n)).length:Math.max(1,toArray(m.y).length)},fail,warn),"pie"===l||"funnel"===l||"gauge"===l||"liquid"===l){needColumn(m.name,"encoding.name","pie"===l||"funnel"===l);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0&&"pie"===l){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===l||"liquid"===l)&&u.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(r,s)}if("radar"===l){const n=needColumn(m.x,"encoding.x",!0),e=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"),n>=0&&e>=0){const i=new Set(u.rows.map(e=>String(e[n]))),o=new Set(u.rows.map(n=>t>=0?String(n[t]):"—")),a=u.rows.filter(n=>isFinite(Number(n[e]))).length,r=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),a<r&&warn("radar-missing-value",`有 ${r-a} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(r,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(r,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(r,s);if("sankey"===l){needColumn(m.source,"encoding.source",!0),needColumn(m.target,"encoding.target",!0);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(r,s)}if("violin"===l||"beeswarm"===l){const n=toArray(m.y);return n.length?(needNumeric(n[0],"encoding.y",!0),"violin"===l&&n.length>1&&warn("violin-single-y",`violin 的 y 只认第一列「${n[0]}」:多组对比请用 encoding.series 拆系列,或改用 beeswarm。`,"encoding.y")):fail("missing-encoding-channel",`encoding.y 是必填的(观测值那一列)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),needColumn(m.x,"encoding.x",!1),finish(r,s)}if("calendar"===l){const n=needColumn(m.x,"encoding.x",!0);if(n>=0){const e=u.rows.filter(e=>!function isCalendarDate(n){return null!=n&&""!==n&&(n instanceof Date?!Number.isNaN(n.getTime()):isFinite(Date.parse(String(n))))}(e[n])).length;e&&warn("calendar-unparseable-date",`${e} 行的日期读不出来(建议写成 "YYYY-MM-DD"),这些格子会被跳过。`,"encoding.x")}return needNumeric(toArray(m.y)[0],"encoding.y",!0),finish(r,s)}if("alluvial"===l){const n=m.axes;return null==n?fail("missing-encoding-channel",`encoding.axes 是必填的(按顺序列出每个轴的列名,至少两个)。可用列:${u.columns.join(" / ")}。`,"encoding.axes"):Array.isArray(n)?n.length<2?fail("alluvial-axes-too-few",`多轴分类流至少要 2 个轴(相邻轴之间才有流量带),encoding.axes 现在只有 ${n.length} 个。可用列:${u.columns.join(" / ")}。`,"encoding.axes"):n.forEach((n,e)=>{const t=`encoding.axes[${e}]`,i=needColumn(n,t,!0);if(i<0)return;const o=new Set(u.rows.map(n=>toLabel(n[i])).filter(n=>""!==n));o.size<2&&warn("alluvial-single-category",`轴「${n}」只有 ${o.size} 个类目,画出来是一条直线、没有流量。`,t);const a=u.rows.filter(n=>{const e=n[i];return null==e||""===e}).length;a&&warn("alluvial-missing-axis-value",`${a} 行在轴「${n}」上是空的,这些行会被跳过。`,t)}):fail("invalid-axes",`encoding.axes 要是列名数组(按顺序),例如 ["${u.columns[0]}", "${u.columns[1]||u.columns[0]}"]。`,"encoding.axes"),m.value&&needNumeric(m.value,"encoding.value",!1),finish(r,s)}if("line"===l||"area"===l||"bar"===l||"scatter"===l||"hexbin"===l){"scatter"===l&&m.size&&needNumeric(m.size,"encoding.size",!1),"hexbin"===l&&needNumeric(m.x,"encoding.x",!0);const n=toArray(m.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(m.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return m.series&&needColumn(m.series,"encoding.series",!1),finish(r,s)}return warn("passthrough-kind",`kind「${l}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(r,s)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const o=a.includes(e.kind);o||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${a.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,a)=>{if(o&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${a} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,a);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${a} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,a)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${a} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,a)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const a="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,a,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,a,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.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:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),a=columnIndex(e,i.value),r=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[a])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?r.slice(0,1):r}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.series),r=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;r.includes(e)||r.push(e);const c=a>=0?toLabel(n[a])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=r.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:r.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:r.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.value||t.color),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);e&&t&&null!==l&&(r.includes(e)||r.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:r},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),a=columnIndex(e,t.total),r=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),r=a>=0?n[a]:void 0;return null!=r&&""!==r&&!1!==r&&0!==Number(r)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.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:r}]}}(n,e);case"violin":return function compileViolin(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=t.series?columnIndex(e,t.series):-1,r=[];for(const n of e.rows){const e=i>=0?toLabel(n[i]):"全部";e&&!r.includes(e)&&r.push(e)}const collect=n=>{const e=new Map;for(const t of n){const n=i>=0?toLabel(t[i]):"全部";if(!n)continue;const a=toNumber(t[o]);null!==a&&(e.has(n)||e.set(n,[]),e.get(n).push(a))}return r.map(n=>e.get(n)||[])};let s;if(a>=0){const n=new Map;for(const t of e.rows){const e=toLabel(t[a])||"(空)";n.has(e)||n.set(e,[]),n.get(e).push(t)}s=[...n.entries()].map(([n,e],t)=>({id:`series-${t+1}`,type:"violin",name:n,data:collect(e)}))}else s=[{id:"series-1",type:"violin",name:firstColumn(t.y),data:collect(e.rows)}];return{legend:{show:s.length>1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:r},yAxis:{name:firstColumn(t.y)},series:s}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),a=columnIndex(e,t.value),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);!e||!t||null===l||l<=0||(r.some(n=>n.name===e)||r.push({name:e}),r.some(n=>n.name===t)||r.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:r,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);case"calendar":return function compileCalendar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=e.rows.map(n=>({date:n[i],value:toNumber(n[o])})).filter(n=>null!==n.date&&void 0!==n.date&&""!==n.date&&null!==n.value);return{legend:{show:!1},tooltip:{trigger:"item"},...n.calendar?{calendar:n.calendar}:{},series:[{id:"calendar",type:"calendar",name:firstColumn(t.y),data:a}]}}(n,e);case"alluvial":return function compileAlluvial(n,e){const t=n.encoding||{},i=(Array.isArray(t.axes)?t.axes:[]).filter(n=>"string"==typeof n&&n),o=e.rows.map(n=>{const t={};return e.columns.forEach((e,i)=>{t[e]=n[i]}),t});return{legend:{show:!1},tooltip:{trigger:"item"},alluvial:{axes:i,rows:o,...t.value?{valueField:t.value}:{},...n.alluvial||{}},series:[{id:"alluvial",type:"alluvial",name:t.value}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),a=toArray(i.y),r=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,c="scatter"===t||"beeswarm"===t||"hexbin"===t,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let m;if(l>=0){const n=new Map,i=columnIndex(e,a[0]);for(const t of e.rows){const e=r?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const a=toLabel(t[l])||"(空)";n.has(a)||n.set(a,new Map),n.get(a).set(e,toNumber(t[i]))}m=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>r?[n,e.get(n)??null]:e.get(n)??null)}))}else m=a.map((n,i)=>{const a=columnIndex(e,n),l=e.rows.map(n=>{const e=r?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[a]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return r||c?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const d={legend:{show:m.length>1},tooltip:{trigger:c?"item":"axis"},series:m};d.crosshair="scatter"===t||"hexbin"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return d.xAxis=r?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},d.yAxis={name:1===a.length?a[0]:void 0},d}(n,e,t)}}(n,e,t))}if(n.matrix&&function isCartesianKind(n){return"line"===n||"area"===n||"bar"===n||"scatter"===n||"violin"===n||"beeswarm"===n||"hexbin"===n}(t)){i.matrix=n.matrix;const e=Array.isArray(i.series)?i.series:[];e.length>1&&e.forEach((n,e)=>{n&&"object"==typeof n&&(n.panel=e)})}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}n.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","waterfall","heatmap","violin","beeswarm","hexbin","calendar","alluvial","sankey","pie","radar","funnel","gauge","liquid","function"],n.CHART_DSL_KINDS=t,n.CHART_DSL_SCHEMA_VERSION=1,n.ChartDslCompileError=ChartDslCompileError,n.chartDslToJsonString=function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))},n.columnIndex=columnIndex,n.compileChartDsl=compileChartDsl,n.formatDiagnostics=function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"},n.numericRatio=numericRatio,n.renderChartDsl=function renderChartDsl(n,t,i){const o=validateChartDsl(t),a=compileChartDsl(t);return{chart:e.createChart(n,a,i),option:a,diagnostics:o}},n.resolveDataset=resolveDataset,n.validateChartDsl=validateChartDsl});
@@ -10,7 +10,7 @@
10
10
  */
11
11
  export declare const CHART_DSL_SCHEMA_VERSION = 1;
12
12
  /** 支持的图表类型。见 `CHART_DSL_COMPILED_KINDS`:没进编译清单的走直通(直接给 `series`)。 */
13
- export declare const CHART_DSL_KINDS: readonly ["line", "area", "bar", "pie", "scatter", "function", "radar", "heatmap", "sankey", "treemap", "gauge", "boxplot", "violin", "beeswarm", "waterfall", "funnel", "graph", "parametric", "liquid"];
13
+ export declare const CHART_DSL_KINDS: readonly ["line", "area", "bar", "pie", "scatter", "function", "radar", "heatmap", "sankey", "treemap", "gauge", "boxplot", "violin", "beeswarm", "hexbin", "calendar", "alluvial", "waterfall", "funnel", "graph", "parametric", "liquid"];
14
14
  export type ChartDslKind = (typeof CHART_DSL_KINDS)[number];
15
15
  /** 有 data/encoding → option 编译路径的类型(其余直通)。 */
16
16
  export declare const CHART_DSL_COMPILED_KINDS: ChartDslKind[];
@@ -46,9 +46,52 @@ export interface ChartDslEncoding {
46
46
  source?: string;
47
47
  /** 桑基图:连线终点列。 */
48
48
  target?: string;
49
+ /**
50
+ * 多轴分类流:每个轴的**列名**(按顺序),至少两个才有「流」。
51
+ *
52
+ * 轴列名和别的通道一样写在 `encoding` 里 —— 一张表 + 「哪几列是轴」就是全部输入,
53
+ * core 的 `alluvial.rows` 由编译器从 `data` 转出来,用户不用自己拍记录对象。
54
+ */
55
+ axes?: string[];
49
56
  }
50
57
  /** 标注的定位轴:`'y'`(默认)画水平线,`'x'` 画垂直线。 */
51
58
  export type ChartDslAnnotationAxis = 'x' | 'y';
59
+ /**
60
+ * 日历热力的观感配置。
61
+ *
62
+ * 只有「格子怎么排、颜色怎么走」这类**与数据无关**的旋钮 —— 日期列与数值列是通道,
63
+ * 写在 `encoding.x` / `encoding.y` 里(与其它 kind 同一口径)。
64
+ */
65
+ export interface ChartDslCalendarOption {
66
+ /** 一周从哪天开始:0 = 周日,1 = 周一(默认)。 */
67
+ weekStart?: 0 | 1;
68
+ /** 是否画左侧的星期标签,默认 true。 */
69
+ weekdayLabels?: boolean;
70
+ /** 是否画顶部的月份标签,默认 true。 */
71
+ monthLabels?: boolean;
72
+ /** 稀(低值)一端的颜色。 */
73
+ minColor?: string;
74
+ /** 密(高值)一端的颜色。 */
75
+ maxColor?: string;
76
+ }
77
+ /**
78
+ * 多轴分类流的观感配置。
79
+ *
80
+ * 轴列名(`encoding.axes`)与流量列(`encoding.value`)是通道,不在这里重复 ——
81
+ * 这里只有排序、间距、配色这类旋钮。
82
+ */
83
+ export interface ChartDslAlluvialOption {
84
+ /** 轴心之间的总跨度占绘图区宽度的比例,默认 0.62。 */
85
+ spread?: number;
86
+ /** 节点条宽度(像素),默认 12。 */
87
+ nodeWidth?: number;
88
+ /** 节点之间的垂直间隙(像素),默认 6。 */
89
+ gap?: number;
90
+ /** 节点排序:按流量降序(默认)或按名字。 */
91
+ sort?: 'total' | 'name';
92
+ /** 连线的不透明度,默认 0.35。 */
93
+ ribbonOpacity?: number;
94
+ }
52
95
  /**
53
96
  * 标注线:目标线 / 阈值线 / 告警线。
54
97
  *
@@ -112,6 +155,18 @@ export interface ChartDslDocument {
112
155
  columns: number | number[];
113
156
  gap?: number;
114
157
  };
158
+ /**
159
+ * 日历热力(`kind: 'calendar'`)的观感配置。
160
+ *
161
+ * 日期列与数值列走 `encoding.x` / `encoding.y`(一行一天),这里只管排布与配色。
162
+ */
163
+ calendar?: ChartDslCalendarOption;
164
+ /**
165
+ * 多轴分类流(`kind: 'alluvial'`)的观感配置。
166
+ *
167
+ * 轴列名写在 `encoding.axes` 里(至少两个),流量列写 `encoding.value`(不写就按每条记录算 1)。
168
+ */
169
+ alluvial?: ChartDslAlluvialOption;
115
170
  /**
116
171
  * 标注图层:目标线 / 阈值线 / 异常点 / 目标区间。
117
172
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damoqiongqiu/ice-chart-dsl",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "license": "MIT",
5
5
  "author": "大漠穷秋",
6
6
  "description": "A JSON-first DSL layer for AI agents to build charts with ice-chart.",
@@ -46,14 +46,14 @@
46
46
  "verify:full": "npm run verify && npm run test:e2e"
47
47
  },
48
48
  "peerDependencies": {
49
- "@damoqiongqiu/ice-chart": "^0.30.13",
49
+ "@damoqiongqiu/ice-chart": "^0.30.16",
50
50
  "ice-render": "^4.2.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@babel/core": "7.28.5",
54
54
  "@babel/preset-env": "7.28.5",
55
55
  "@babel/preset-typescript": "7.28.5",
56
- "@damoqiongqiu/ice-chart": "^0.30.13",
56
+ "@damoqiongqiu/ice-chart": "^0.30.16",
57
57
  "@playwright/test": "^1.63.0",
58
58
  "@rollup/plugin-babel": "6.1.0",
59
59
  "@rollup/plugin-commonjs": "28.0.1",
@@ -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.2.1"
4
+ version: "0.2.3"
5
5
  category: ux
6
6
  platforms:
7
7
  - claude-code
@@ -28,6 +28,8 @@ This SKILL is the right choice for:
28
28
  - tables with a second category dimension: radar (indicators), heatmap (row × column matrix)
29
29
  - staged totals: waterfall (with a total row)
30
30
  - flow tables: sankey (source / target / value)
31
+ - N-axis categorical flow: alluvial (a table fanned across 2+ axis columns)
32
+ - day-by-day heat grids: calendar (one row per date)
31
33
  - single-value cards: funnel / gauge / liquid
32
34
  - math curves: `kind: "function"` (e.g. `sin(x)/x`, damped oscillation)
33
35
  - charts that must be interactive afterwards (hover, zoom, brush, legend toggle, serialization)
@@ -50,7 +52,7 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
50
52
  ```json
51
53
  {
52
54
  "schemaVersion": 1,
53
- "kind": "line | area | bar | pie | scatter | violin | beeswarm | function | (passthrough kinds)",
55
+ "kind": "line | area | bar | pie | scatter | violin | beeswarm | hexbin | calendar | alluvial | function | (passthrough kinds)",
54
56
  "matrix": { "rows": 2, "columns": 3, "gap": 12 },
55
57
  "title": "可选",
56
58
  "data": { "columns": ["月份", "销量", "渠道"], "rows": [["1月", 120, "线上"]] },
@@ -78,6 +80,20 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
78
80
  group, shared scales, hover/brush/zoom resolved per panel. Panel indexes are assigned by the
79
81
  compiler; warnings (`matrix-single-panel`, `matrix-too-few-panels`) tell you when faceting has
80
82
  nothing to split or too few panels.
83
+ - `encoding.x` + `encoding.y` (both numeric, optional `encoding.size` as weight) make a `hexbin`
84
+ (hexagonal binning): the two-column point table is compiled as-is, the pointer interacts with
85
+ **cells** (count / sum / mean / max), and xy crosshair is on. Tuning (radius, aggregation, colors)
86
+ goes through the `series` passthrough.
87
+ - `encoding.x` (date column) + `encoding.y` (numeric column) make a `calendar` (one cell per day).
88
+ Dates are **not parsed by the DSL** — ice-chart aligns them by UTC Y/M/D and sums same-day rows,
89
+ so `Date` objects and loose strings like `2026-1-5` still work. Unreadable cells raise the
90
+ `calendar-unparseable-date` **warning** (with a row count), never a compile error.
91
+ Layout and colors go to the top-level `calendar` field (`weekStart`, `weekdayLabels`, `minColor`, …).
92
+ - `encoding.axes` (2+ column names, in order) + optional `encoding.value` make an `alluvial`
93
+ (multi-axis categorical flow — a **table fanned across axes**, unlike sankey's graph topology).
94
+ ice-chart wants `alluvial.rows` as record objects; that conversion happens in the compiler, so you
95
+ pass the same table you would for any other kind. Without `encoding.value` every record counts as 1.
96
+ Ordering / spacing / colors go to the top-level `alluvial` field (`sort`, `spread`, `nodeWidth`, …).
81
97
  - `kind: "function"` needs `expression` (+ optional `domain`, `params`) and no data.
82
98
 
83
99
  ## Annotations (goal lines / thresholds / target bands)
@@ -142,6 +158,8 @@ Example feedback:
142
158
  ## Reference
143
159
 
144
160
  - package: `@damoqiongqiu/ice-chart-dsl` (npm)
145
- - runtime peer dependencies: `@damoqiongqiu/ice-chart@^0.30.1`(标注需要 0.30.1 起)+
161
+ - runtime peer dependencies: `@damoqiongqiu/ice-chart@^0.30.16`(分布组图 `violin` / `beeswarm`
162
+ 与面板矩阵 `matrix` 是 0.30.13 起,六边形分箱 `hexbin` 是 0.30.14 起,
163
+ 日历热力 `calendar` / 多轴分类流 `alluvial` 是 0.30.16 起;只画基础图用 0.30.1 也够)+
146
164
  `ice-render@^4.2.0`(家族当前引擎 `4.3.0`,建议直接装最新)
147
165
  - schema: `src/schema/chart-dsl.schema.json`