@damoqiongqiu/ice-chart-dsl 0.6.1 → 0.6.3

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
@@ -78,6 +78,8 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
78
78
  | `line` / `area` | `x` + `y`(+ `series`) | 数值 x 自动用数值轴 + `[x, y]` 数据点 |
79
79
  | `bar` | `x` + `y`(+ `series`) | 堆叠用 `options.stack` |
80
80
  | `scatter` | `x` + `y`(+ `size`) | 绑 `size` 即气泡图 |
81
+ | `beeswarm` | `x`(分组)+ `y`(观测值) | 逐点编成 `[组, 值]`,同一组内自动避让;提示框按数据项触发 |
82
+ | `violin` | `x`(分组)+ `y`(观测值) | 按组把观测值收成 `number[][]`,core 算密度轮廓;多组对比用 `series` 拆系列 |
81
83
  | `pie` | `name` + `value` | 负值会被警告(饼图不表达负值) |
82
84
  | `radar` | `x`(指标)+ `y`(数值)+ `series` | 指标名从 x 列推,上限自动取整到好看的刻度 |
83
85
  | `heatmap` | `x` + `y`(两个类目列)+ `value` | 二维矩阵表直接画 |
@@ -89,6 +91,23 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
89
91
  **直通**:`treemap` / `graph` / `parametric` / `boxplot` 等直接给 `series`(`options` 照常透传)。
90
92
  **逃生舱**:`options` 里的键覆盖编译结果(`series` 除外),所以 DSL 跟不上核心演进时不会把人堵死。
91
93
 
94
+ **面板矩阵(数据驱动分面)**:顶层加 `matrix: { rows, columns, gap? }`,DSL 会把
95
+ `encoding.series` 拆出来的每个分组依次放进一块面板(多列 `y` 同理,每列一块)——
96
+ 「一张表按渠道拆成六块」是**意图**,面板下标由编译器分配,用户不用自己数。
97
+
98
+ ```json
99
+ {
100
+ "kind": "line",
101
+ "matrix": { "rows": 2, "columns": 3, "gap": 12 },
102
+ "data": { "columns": ["月份", "销量", "渠道"], "rows": [["1月", 120, "线上"], ["1月", 90, "线下"]] },
103
+ "encoding": { "x": "月份", "y": "销量", "series": "渠道" }
104
+ }
105
+ ```
106
+
107
+ 只有直角坐标的 kind 才有面板语义(其余场景会被警告并忽略);只拆出一块面板、或分组数超过
108
+ 面板数,都会给可执行的警告(`matrix-single-panel` / `matrix-too-few-panels`)。
109
+ 分布组图的细调(`violin.bandwidth` / `beeswarm.spread` 等)走 `series` 直通 —— 那属于 ChartOption 的活。
110
+
92
111
  ## 标注:目标线 / 阈值线 / 异常点 / 目标区间
93
112
 
94
113
  业务图上最高频的参考线不是「一种新图表」,而是一条配置 —— 顶层 `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","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 firstColumn(n){return Array.isArray(n)?n[0]:n}const t=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series"],i=["x","y","series","size","color","name","value","total","source","target"],o=["line","area","bar","scatter"];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 u=[],c=64;try{for(let o=0;o<=c;o++){const r=s+(l-s)*o/c,a={...i,[t]:r},m=n.evaluateExpression(e,a);u.push("number"==typeof m?m:NaN)}}catch(n){return[]}return u}(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 u=resolveDataset(o);if(void 0===o.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(r,a);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(r,a);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(r,a);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(r,a);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 c=o.encoding&&"object"==typeof o.encoding?o.encoding:{};o.encoding||fail("missing-encoding",`kind「${s}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const n of Object.keys(c))i.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const m=c.x?columnIndex(u,c.x):-1;validateAnnotation(o.annotation,{kind:s,dataset:u,xIndex:m,numericX:m>=0&&numericRatio(u,m)>.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("pie"===s||"funnel"===s||"gauge"===s||"liquid"===s){needColumn(c.name,"encoding.name","pie"===s||"funnel"===s);const n=needNumeric(c.value,"encoding.value",!0);if(n>=0&&"pie"===s){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"===s||"liquid"===s)&&u.rows.length>1&&warn("single-value-kind",`kind「${s}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(r,a)}if("radar"===s){const n=needColumn(c.x,"encoding.x",!0),e=needNumeric(toArray(c.y)[0],"encoding.y",!0),t=c.series?needColumn(c.series,"encoding.series",!1):-1;if(toArray(c.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(r,a)}if("heatmap"===s)return needColumn(c.x,"encoding.x",!0),needColumn(c.y,"encoding.y",!0),needNumeric(c.value||c.color,c.value?"encoding.value":"encoding.color",!0),finish(r,a);if("waterfall"===s)return needColumn(c.name,"encoding.name",!0),needNumeric(c.value,"encoding.value",!0),c.total&&needColumn(c.total,"encoding.total",!1),finish(r,a);if("sankey"===s){needColumn(c.source,"encoding.source",!0),needColumn(c.target,"encoding.target",!0);const n=needNumeric(c.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,a)}if("line"===s||"area"===s||"bar"===s||"scatter"===s){"scatter"===s&&c.size&&needNumeric(c.size,"encoding.size",!1);const n=toArray(c.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(c.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return c.series&&needColumn(c.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 u=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(u)||s.set(u,new Map),s.get(u).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"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,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let c;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]))}c=[...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 c=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?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const m={legend:{show:c.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:c};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},m.yAxis={name:1===r.length?r[0]:void 0},m}(n,e,t)}}(n,e,t))}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","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","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;
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","waterfall","funnel","graph","parametric","liquid"],r=["line","area","bar","scatter","waterfall","heatmap","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 firstColumn(n){return Array.isArray(n)?n[0]:n}const a=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series"],s=["x","y","series","size","color","name","value","total","source","target"],l=["line","area","bar","scatter"];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,u=Array.isArray(t.series)&&t.series.length>0;if(u&&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:{},u=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 u=[],c=64;try{for(let o=0;o<=c;o++){const r=s+(l-s)*o/c,a={...i,[t]:r},m=e(n,a);u.push("number"==typeof m?m:NaN)}}catch(n){return[]}return u}(o,a,s,t.domain,l);for(const e of n(o,{variable:a,params:s,values:u})){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(u)return validateAnnotation(t.annotation,{kind:l,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(i,r);const c=resolveDataset(t);if(void 0===t.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(i,r);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(i,r);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(i,r);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(i,r);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 m=t.encoding&&"object"==typeof t.encoding?t.encoding:{};t.encoding||fail("missing-encoding",`kind「${l}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.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(c,m.x):-1;validateAnnotation(t.annotation,{kind:l,dataset:c,xIndex:d,numericX:d>=0&&numericRatio(c,d)>.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("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=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"===l||"liquid"===l)&&c.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${c.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(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(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=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(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 是必填的(一个列名或一组列名)。可用列:${c.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 u=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(u)||s.set(u,new Map),s.get(u).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"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,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let c;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]))}c=[...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 c=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?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const m={legend:{show:c.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:c};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},m.yAxis={name:1===r.length?r[0]:void 0},m}(n,e,t)}}(n,e,t))}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","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};
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","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 firstColumn(n){return Array.isArray(n)?n[0]:n}const i=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series"],o=["x","y","series","size","color","name","value","total","source","target"],a=["line","area","bar","scatter"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(n){const a=[],r=[],fail=(n,e,t)=>a.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>r.push({severity:"warning",code:n,message:e,path:t});if(!n||"object"!=typeof n||Array.isArray(n))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(a,r);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,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 u=[],c=64;try{for(let o=0;o<=c;o++){const a=s+(l-s)*o/c,r={...i,[t]:a},m=e.evaluateExpression(n,r);u.push("number"==typeof m?m:NaN)}}catch(n){return[]}return u}(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?a:r).push(e)}}else fail("missing-expression",`kind「${s}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(a,r)}if(l)return validateAnnotation(n.annotation,{kind:s,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(a,r);const u=resolveDataset(n);if(void 0===n.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(a,r);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(a,r);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(a,r);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(a,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 c=n.encoding&&"object"==typeof n.encoding?n.encoding:{};n.encoding||fail("missing-encoding",`kind「${s}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const n of Object.keys(c))o.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const m=c.x?columnIndex(u,c.x):-1;validateAnnotation(n.annotation,{kind:s,dataset:u,xIndex:m,numericX:m>=0&&numericRatio(u,m)>.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("pie"===s||"funnel"===s||"gauge"===s||"liquid"===s){needColumn(c.name,"encoding.name","pie"===s||"funnel"===s);const n=needNumeric(c.value,"encoding.value",!0);if(n>=0&&"pie"===s){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"===s||"liquid"===s)&&u.rows.length>1&&warn("single-value-kind",`kind「${s}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(a,r)}if("radar"===s){const n=needColumn(c.x,"encoding.x",!0),e=needNumeric(toArray(c.y)[0],"encoding.y",!0),t=c.series?needColumn(c.series,"encoding.series",!1):-1;if(toArray(c.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(a,r)}if("heatmap"===s)return needColumn(c.x,"encoding.x",!0),needColumn(c.y,"encoding.y",!0),needNumeric(c.value||c.color,c.value?"encoding.value":"encoding.color",!0),finish(a,r);if("waterfall"===s)return needColumn(c.name,"encoding.name",!0),needNumeric(c.value,"encoding.value",!0),c.total&&needColumn(c.total,"encoding.total",!1),finish(a,r);if("sankey"===s){needColumn(c.source,"encoding.source",!0),needColumn(c.target,"encoding.target",!0);const n=needNumeric(c.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,r)}if("line"===s||"area"===s||"bar"===s||"scatter"===s){"scatter"===s&&c.size&&needNumeric(c.size,"encoding.size",!1);const n=toArray(c.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(c.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return c.series&&needColumn(c.series,"encoding.series",!1),finish(a,r)}return warn("passthrough-kind",`kind「${s}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(a,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=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 u=a>=0?toLabel(n[a])||"(空)":String(firstColumn(t.y));s.has(u)||s.set(u,new Map),s.get(u).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"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);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,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let c;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]))}c=[...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 c=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?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const m={legend:{show:c.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:c};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=r?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},m.yAxis={name:1===a.length?a[0]:void 0},m}(n,e,t)}}(n,e,t))}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","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});
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});
@@ -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", "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", "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[];
@@ -100,6 +100,18 @@ export interface ChartDslDocument {
100
100
  kind: ChartDslKind;
101
101
  data?: ChartDslDataset;
102
102
  encoding?: ChartDslEncoding;
103
+ /**
104
+ * 面板矩阵(小倍数):把按 `encoding.series` 拆出来的每个系列各放进一块面板。
105
+ *
106
+ * 这是**数据驱动分面**在 DSL 这一层的样子 —— 「一张表按渠道拆成六块」是意图,
107
+ * 不是 ChartOption 的字段搬运;面板下标由编译器分配,用户不用自己算。
108
+ * 只对直角坐标的 kind 有意义(其余场景会被警告并忽略)。
109
+ */
110
+ matrix?: {
111
+ rows: number | number[];
112
+ columns: number | number[];
113
+ gap?: number;
114
+ };
103
115
  /**
104
116
  * 标注图层:目标线 / 阈值线 / 异常点 / 目标区间。
105
117
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damoqiongqiu/ice-chart-dsl",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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.29.1",
49
+ "@damoqiongqiu/ice-chart": "^0.30.13",
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.29.1",
56
+ "@damoqiongqiu/ice-chart": "^0.30.13",
57
57
  "@playwright/test": "^1.63.0",
58
58
  "@rollup/plugin-babel": "6.1.0",
59
59
  "@rollup/plugin-commonjs": "28.0.1",
@@ -63,7 +63,7 @@
63
63
  "@types/jest": "29.5.14",
64
64
  "babel-jest": "29.7.0",
65
65
  "http-server": "^14.1.1",
66
- "ice-render": "^4.2.0",
66
+ "ice-render": "^4.3.0",
67
67
  "jest": "29.7.0",
68
68
  "rimraf": "5.0.10",
69
69
  "rollup": "3.29.5",
@@ -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.0"
4
+ version: "0.2.1"
5
5
  category: ux
6
6
  platforms:
7
7
  - claude-code
@@ -50,7 +50,8 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
50
50
  ```json
51
51
  {
52
52
  "schemaVersion": 1,
53
- "kind": "line | area | bar | pie | scatter | function | (passthrough kinds)",
53
+ "kind": "line | area | bar | pie | scatter | violin | beeswarm | function | (passthrough kinds)",
54
+ "matrix": { "rows": 2, "columns": 3, "gap": 12 },
54
55
  "title": "可选",
55
56
  "data": { "columns": ["月份", "销量", "渠道"], "rows": [["1月", 120, "线上"]] },
56
57
  "encoding": { "x": "月份", "y": "销量", "series": "渠道" },
@@ -70,6 +71,13 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
70
71
  - `encoding.x` + `encoding.y` + `encoding.series` make a `radar` (x holds the indicator names).
71
72
  - `encoding.x` + `encoding.y` (two category columns) + `encoding.value` make a `heatmap`.
72
73
  - `encoding.source` + `encoding.target` + `encoding.value` make a `sankey`.
74
+ - `encoding.x` (group) + `encoding.y` (observations) make a `violin` (one density contour per
75
+ group; extra `series` splits it into several distributions) or a `beeswarm` (one dot per
76
+ observation, dodged inside its group). Both are plain "detail table" inputs — no hand-built arrays.
77
+ - `matrix` turns `encoding.series` (or several `y` columns) into **small multiples**: one panel per
78
+ group, shared scales, hover/brush/zoom resolved per panel. Panel indexes are assigned by the
79
+ compiler; warnings (`matrix-single-panel`, `matrix-too-few-panels`) tell you when faceting has
80
+ nothing to split or too few panels.
73
81
  - `kind: "function"` needs `expression` (+ optional `domain`, `params`) and no data.
74
82
 
75
83
  ## Annotations (goal lines / thresholds / target bands)
@@ -98,7 +106,7 @@ an SLA threshold, an event marker, a target band. That is one field, not a new c
98
106
  category names (or indexes) on a category axis, timestamps or date strings on a time axis.
99
107
  - Annotations live on the coordinate system, so they follow zoom / pan, stay out of the legend,
100
108
  do not occupy data indexes, and never steal hit-testing from the series.
101
- - Only cartesian kinds (`line` / `area` / `bar` / `scatter`) can host annotations; using them on
109
+ - Only cartesian kinds (`line` / `area` / `bar` / `scatter` / `violin` / `beeswarm`) can host annotations; using them on
102
110
  `pie` / `radar` / `sankey` / … raises `annotation-non-cartesian` (those scenes have no x/y axes).
103
111
  - Structural mistakes are **errors** (`missing-annotation-value`: missing `value`, or `from` without
104
112
  `to`); suspicious values are **warnings** (`annotation-unknown-category`,
@@ -134,6 +142,6 @@ Example feedback:
134
142
  ## Reference
135
143
 
136
144
  - package: `@damoqiongqiu/ice-chart-dsl` (npm)
137
- - runtime peer dependencies: `@damoqiongqiu/ice-chart@^0.20.0` (标注需要 0.20.0 起)+
138
- `ice-render@^2.3.0`(家族当前引擎 `2.3.0`,建议直接装最新)
145
+ - runtime peer dependencies: `@damoqiongqiu/ice-chart@^0.30.1`(标注需要 0.30.1 起)+
146
+ `ice-render@^4.2.0`(家族当前引擎 `4.3.0`,建议直接装最新)
139
147
  - schema: `src/schema/chart-dsl.schema.json`