@damoqiongqiu/ice-chart-dsl 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,40 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
90
90
  **直通**:`treemap` / `graph` / `parametric` / `boxplot` 等直接给 `series`(`options` 照常透传)。
91
91
  **逃生舱**:`options` 里的键覆盖编译结果(`series` 除外),所以 DSL 跟不上核心演进时不会把人堵死。
92
92
 
93
+ ## 标注:目标线 / 阈值线 / 异常点 / 目标区间
94
+
95
+ 业务图上最高频的参考线不是「一种新图表」,而是一条配置 —— 顶层 `annotation`(与 `encoding` 平级):
96
+
97
+ ```json
98
+ {
99
+ "schemaVersion": 1,
100
+ "kind": "line",
101
+ "title": "月度销量与目标",
102
+ "data": { "columns": ["月份", "销量"], "rows": [["1月", 120], ["2月", 132], ["3月", 101]] },
103
+ "encoding": { "x": "月份", "y": "销量" },
104
+ "annotation": {
105
+ "lines": [
106
+ { "axis": "y", "value": 150, "text": "目标 150" },
107
+ { "axis": "y", "value": 100, "text": "告警阈值", "color": "#dc3545" },
108
+ { "axis": "x", "value": "2月", "text": "上线" }
109
+ ],
110
+ "points": [{ "x": "3月", "y": 101, "text": "异常点", "symbol": "diamond" }],
111
+ "areas": [{ "axis": "y", "from": 0, "to": 100, "text": "达标区" }]
112
+ }
113
+ }
114
+ ```
115
+
116
+ - `lines[].value` / `areas[].from`、`to` / `points[].x`、`y` 都是**数据值**(不是像素):
117
+ 数值轴写数字、类目轴写类目名或下标、时间轴写时间戳或日期串。
118
+ - 它挂在坐标系上,所以**跟着缩放 / 平移走**,不进图例、不占数据下标、不抢命中测试;
119
+ 越界的标注不画,原因由 ice-chart 的 `chart.annotationDiagnostics()` 给出。
120
+ - 只对直角坐标的 kind 有意义(`line` / `area` / `bar` / `scatter`);
121
+ 给饼图 / 雷达 / 桑基等会被警告 `annotation-non-cartesian`。
122
+ - 编译期诊断:缺 `value` / 缺 `from`+`to` 是**错误**(带 `annotation.lines[0].value` 这样的路径),
123
+ 类目不存在(`annotation-unknown-category`)、数值轴写了非数字(`annotation-value-type`)、
124
+ 区间宽度为 0(`annotation-empty-area`)是**警告**。
125
+ - 逃生舱照常:`options.annotation` 能整体覆盖顶层 `annotation`。
126
+
93
127
  ## 诊断:给 agent 的自修复反馈
94
128
 
95
129
  `validateChartDsl()` **不抛异常**,返回结构化诊断(`{ severity, code, message, path }`):
@@ -101,6 +135,8 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
101
135
  [警告] 饼图 / 玫瑰图不适合表达负值,检测到 2 行负数。(encoding.value)
102
136
  [错误] 表达式错误:缺少右括号(位置 6)… (expression)
103
137
  [警告] 参数「b」定义了但表达式没有用到。(expression)
138
+ [错误] annotation.lines[0] 缺少 value(数值轴写数字、类目轴写类目名或下标)。(annotation.lines[0].value)
139
+ [警告] annotation.lines[1].value 的类目名「13月」不在 x 列里(该列有 3 个类目),这条标注不会画出来。(annotation.lines[1].value)
104
140
  ```
105
141
 
106
142
  公式类会**采样一遍再诊断**:静态检查抓语法错误,运行层抓「整段开不出来」「输出恒定」——
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("@damoqiongqiu/ice-chart");const n=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(e){const n=e&&e.data;if(!n)return null;if(Array.isArray(n)){const e=n.filter(e=>e&&"object"==typeof e&&!Array.isArray(e));if(!e.length)return{columns:[],rows:[]};const t=[];for(const n of e)for(const e of Object.keys(n))t.includes(e)||t.push(e);return{columns:t,rows:e.map(e=>t.map(n=>e[n]))}}if("object"!=typeof n)return null;const t=Array.isArray(n.columns)?n.columns.map(e=>String(e)):[],r=Array.isArray(n.rows)?n.rows:[];return t.length?{columns:t,rows:r.map(e=>Array.isArray(e)?e:[])}:null}function columnIndex(e,n){return e&&n?e.columns.indexOf(n):-1}function isNumericCell(e){return"number"==typeof e?isFinite(e):"string"==typeof e&&""!==e.trim()&&isFinite(Number(e))}function numericRatio(e,n){let t=0,r=0;for(const i of e.rows){const e=i[n];null!=e&&""!==e&&(t+=1,isNumericCell(e)&&(r+=1))}return t?r/t:0}function toNumber(e){if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e&&""!==e.trim()){const n=Number(e);return isFinite(n)?n:null}return null}function toLabel(e){return null==e?"":String(e)}function toArray(e){return null==e?[]:Array.isArray(e)?e.filter(e=>"string"==typeof e&&e):[e]}function firstColumn(e){return Array.isArray(e)?e[0]:e}const t=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],r=["x","y","series","size","color","name","value","total","source","target"];function validateChartDsl(i){const s=[],o=[],fail=(e,n,t)=>s.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>o.push({severity:"warning",code:e,message:n,path:t});if(!i||"object"!=typeof i||Array.isArray(i))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(s,o);void 0!==i.schemaVersion&&1!==i.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${i.schemaVersion}(当前是 1)。`,"schemaVersion"),i.kind?n.includes(i.kind)||fail("unsupported-kind",`不支持的 kind「${i.kind}」。可用类型:${n.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${n.join(" / ")}。`,"kind");for(const e of Object.keys(i))t.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const a=i.kind,l=Array.isArray(i.series)&&i.series.length>0;if(l&&i.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===a||"parametric"===a){const n=String(i.expression||"");if(n.trim()){const t="function"===a?"x":"t",r=i.params&&"object"==typeof i.params?i.params:{},l=function sampleExpression(n,t,r,i,s){const o="function"===s?[-10,10]:[0,2*Math.PI],a=Array.isArray(i)&&isFinite(Number(i[0]))?Number(i[0]):o[0],l=Array.isArray(i)&&isFinite(Number(i[1]))?Number(i[1]):o[1];if(!(l>a))return[];const c=[],u=64;try{for(let i=0;i<=u;i++){const s=a+(l-a)*i/u,o={...r,[t]:s},m=e.evaluateExpression(n,o);c.push("number"==typeof m?m:NaN)}}catch(e){return[]}return c}(n,t,r,i.domain,a);for(const i of e.diagnoseExpression(n,{variable:t,params:r,values:l})){const e={severity:i.severity,code:`expression-${i.code}`,message:i.message,path:"expression"};("error"===i.severity?s:o).push(e)}}else fail("missing-expression",`kind「${a}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(s,o)}if(l)return finish(s,o);const c=resolveDataset(i);if(void 0===i.data)return fail("missing-data",`kind「${a}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(s,o);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(s,o);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(s,o);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(s,o);c.rows.forEach((e,n)=>{e.length!==c.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${n}]`)});const u=i.encoding&&"object"==typeof i.encoding?i.encoding:{};i.encoding||fail("missing-encoding",`kind「${a}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.columns[0]}" }。`,"encoding");for(const e of Object.keys(u))r.includes(e)||warn("unknown-encoding",`encoding 里的未知通道「${e}」会被忽略。`,`encoding.${e}`);const needColumn=(e,n,t)=>{if(!e)return t&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${c.columns.join(" / ")}。`,n),-1;const r=columnIndex(c,e);return r<0?(fail("unknown-column",`列「${e}」不存在。可用列:${c.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(c,r)?r:(fail("empty-column",`列「${e}」整列都是空值。`,n),-1)},needNumeric=(e,n,t)=>{const r=needColumn(e,n,t);if(r<0)return-1;const i=numericRatio(c,r);return i<.6?(fail("non-numeric-column",`列「${e}」不是数值列(数字占比 ${(100*i).toFixed(0)}%),不能当 ${n.split(".")[1]} 用。`,n),-1):(i<1&&warn("partly-non-numeric",`列「${e}」有 ${Math.round(100*(1-i))}% 的值不是数字,这些点会被当作空缺。`,n),r)};if("pie"===a||"funnel"===a||"gauge"===a||"liquid"===a){needColumn(u.name,"encoding.name","pie"===a||"funnel"===a);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0&&"pie"===a){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return e>=0&&("gauge"===a||"liquid"===a)&&c.rows.length>1&&warn("single-value-kind",`kind「${a}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(s,o)}if("radar"===a){const e=needColumn(u.x,"encoding.x",!0),n=needNumeric(toArray(u.y)[0],"encoding.y",!0),t=u.series?needColumn(u.series,"encoding.series",!1):-1;if(toArray(u.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),e>=0&&n>=0){const r=new Set(c.rows.map(n=>String(n[e]))),i=new Set(c.rows.map(e=>t>=0?String(e[t]):"—")),s=c.rows.filter(e=>isFinite(Number(e[n]))).length,o=r.size*i.size;r.size<3&&warn("radar-too-few-indicators",`只有 ${r.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),s<o&&warn("radar-missing-value",`有 ${o-s} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(s,o)}if("heatmap"===a)return needColumn(u.x,"encoding.x",!0),needColumn(u.y,"encoding.y",!0),needNumeric(u.value||u.color,u.value?"encoding.value":"encoding.color",!0),finish(s,o);if("candlestick"===a){needColumn(u.x,"encoding.x",!0);const e=toArray(u.y);if(4!==e.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${e.length} 列)。`,"encoding.y");else for(const n of e)needNumeric(n,"encoding.y",!0);return finish(s,o)}if("waterfall"===a)return needColumn(u.name,"encoding.name",!0),needNumeric(u.value,"encoding.value",!0),u.total&&needColumn(u.total,"encoding.total",!1),finish(s,o);if("sankey"===a){needColumn(u.source,"encoding.source",!0),needColumn(u.target,"encoding.target",!0);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<=0}).length;n&&warn("sankey-non-positive",`${n} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(s,o)}if("line"===a||"area"===a||"bar"===a||"scatter"===a){"scatter"===a&&u.size&&needNumeric(u.size,"encoding.size",!1);const e=toArray(u.y);e.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),e.length>8&&warn("too-many-series",`y 绑定了 ${e.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(u.x,"encoding.x",!0);for(const n of e)needNumeric(n,"encoding.y",!0);return u.series&&needColumn(u.series,"encoding.series",!1),finish(s,o)}return warn("passthrough-kind",`kind「${a}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(s,o)}function finish(e,n){return{valid:0===e.length,errors:e,warnings:n}}class ChartDslCompileError extends Error{constructor(e){super(`[ice-chart-dsl] DSL 校验不通过:\n${e.map(e=>`- ${e.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=e}}function compileChartDsl(e){const n=validateChartDsl(e);if(!n.valid)throw new ChartDslCompileError(n.errors);const t=e.kind,r={title:normalizeTitle(e),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(e.series)&&e.series.length)r.series=e.series;else if("function"===t||"parametric"===t)r.legend={show:!1},r.tooltip={trigger:"item"},r.crosshair={show:!0,axis:"x",showAxisLabel:!0},r.xAxis={type:"value",name:"x"},r.yAxis={name:"y"},r.series=[{id:"curve",type:t,name:e.title&&"object"==typeof e.title?e.title.text:String(e.title||("function"===t?"f(x)":"curve")),expression:e.expression,domain:e.domain,params:e.params,lineWidth:2}];else{const n=resolveDataset(e);Object.assign(r,function compileByKind(e,n,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(e,n,t){const r=e.encoding||{},i=columnIndex(n,r.name),s=columnIndex(n,r.value),o=n.rows.map(e=>({name:toLabel(e[i]),value:toNumber(e[s])})).filter(e=>""!==e.name&&isFinite(e.value)),a="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:r.value,data:a?o.slice(0,1):o}]}}(e,n,t);case"radar":return function compileRadar(e,n){const t=e.encoding||{},r=columnIndex(n,t.x),i=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.series),o=[],a=new Map;for(const e of n.rows){const n=toLabel(e[r]),l=toNumber(e[i]);if(!n||null===l)continue;o.includes(n)||o.push(n);const c=s>=0?toLabel(e[s])||"(空)":String(firstColumn(t.y));a.has(c)||a.set(c,new Map),a.get(c).set(n,l)}const l=o.map(e=>{let n=0;for(const t of a.values())n=Math.max(n,Math.abs(t.get(e)??0));return function niceCeil(e){if(!isFinite(e)||e<=0)return 1;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<=1?1:t<=2?2:t<=5?5:10)*n}(n)});return{legend:{show:a.size>1},tooltip:{trigger:"item"},radar:{indicators:o.map((e,n)=>({name:e,max:l[n]})),splitNumber:4},series:[...a.entries()].map(([e,n],t)=>({id:`radar-${t+1}`,type:"radar",name:e,data:o.map(e=>n.get(e)??0)}))}}(e,n);case"heatmap":return function compileHeatmap(e,n){const t=e.encoding||{},r=columnIndex(n,t.x),i=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.value||t.color),o=[],a=[];for(const e of n.rows){const n=toLabel(e[r]),t=toLabel(e[i]),l=toNumber(e[s]);n&&t&&null!==l&&(o.includes(n)||o.push(n),a.push([n,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:o},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:a}]}}(e,n);case"candlestick":return function compileCandlestick(e,n){const t=e.encoding||{},r=columnIndex(n,t.x),[i,s,o,a]=toArray(t.y).map(e=>columnIndex(n,e)),l=[],c=[];for(const e of n.rows){const n=toLabel(e[r]),t=toNumber(e[i]),u=toNumber(e[s]),m=toNumber(e[o]),d=toNumber(e[a]);n&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(n)||l.push(n),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(e,n);case"waterfall":return function compileWaterfall(e,n){const t=e.encoding||{},r=columnIndex(n,t.name),i=columnIndex(n,t.value),s=columnIndex(n,t.total),o=n.rows.map(e=>{const n=toLabel(e[r]),t=toNumber(e[i]),o=s>=0?e[s]:void 0;return null!=o&&""!==o&&!1!==o&&0!==Number(o)?{name:n,value:isFinite(t)?t:0,total:!0}:{name:n,value:t}}).filter(e=>""!==e.name&&(null===e.value||isFinite(e.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:o}]}}(e,n);case"sankey":return function compileSankey(e,n){const t=e.encoding||{},r=columnIndex(n,t.source),i=columnIndex(n,t.target),s=columnIndex(n,t.value),o=[],a=[];for(const e of n.rows){const n=toLabel(e[r]),t=toLabel(e[i]),l=toNumber(e[s]);!n||!t||null===l||l<=0||(o.some(e=>e.name===n)||o.push({name:n}),o.some(e=>e.name===t)||o.push({name:t}),a.push({source:n,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:o,links:a,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(e,n);default:return function compileCartesian(e,n,t){const r=e.encoding||{},i=columnIndex(n,r.x),s=toArray(r.y),o=i>=0&&numericRatio(n,i)>.9,a=r.size?columnIndex(n,r.size):-1,l=r.series?columnIndex(n,r.series):-1,c=[],pushX=e=>{c.some(n=>n===e)||c.push(e)};let u;if(l>=0){const e=new Map,r=columnIndex(n,s[0]);for(const t of n.rows){const n=o?toNumber(t[i]):toLabel(t[i]);if(""===n||null===n)continue;pushX(n);const s=toLabel(t[l])||"(空)";e.has(s)||e.set(s,new Map),e.get(s).set(n,toNumber(t[r]))}u=[...e.entries()].map(([e,n],r)=>({id:`series-${r+1}`,type:t,name:e,data:c.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,r)=>{const s=columnIndex(n,e),l=n.rows.map(e=>{const n=o?toNumber(e[i]):toLabel(e[i]),t=toNumber(e[s]);if(""===n||null===n||null===t)return null;if(pushX(n),a>=0){const r=toNumber(e[a]);return[n,t,null===r?8:r]}return o?[n,t]:t}).filter(e=>null!==e);return{id:`series-${r+1}`,type:t,name:e,data:l}});const m={legend:{show:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=o?{type:"value",name:r.x}:{type:"category",name:r.x,data:c},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,t)}}(e,n,t))}if(e.options&&"object"==typeof e.options)for(const n of Object.keys(e.options)){const t=e.options[n];void 0!==t&&("series"!==n&&(r[n]=t))}return r}function normalizeTitle(e){if(e.title)return"string"==typeof e.title?{text:e.title}:e.title}exports.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","candlestick","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","function"],exports.CHART_DSL_KINDS=n,exports.CHART_DSL_SCHEMA_VERSION=1,exports.ChartDslCompileError=ChartDslCompileError,exports.chartDslToJsonString=function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))},exports.columnIndex=columnIndex,exports.compileChartDsl=compileChartDsl,exports.formatDiagnostics=function formatDiagnostics(e){const n=[...e.errors,...e.warnings].map(e=>{const n="error"===e.severity?"错误":"警告",t=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${t}`});return n.length?n.join("\n"):"✓ DSL 校验通过"},exports.numericRatio=numericRatio,exports.renderChartDsl=function renderChartDsl(n,t,r){const i=validateChartDsl(t),s=compileChartDsl(t);return{chart:e.createChart(n,s,r),option:s,diagnostics:i}},exports.resolveDataset=resolveDataset,exports.validateChartDsl=validateChartDsl;
1
+ "use strict";var n=require("@damoqiongqiu/ice-chart");const e=["line","area","bar","pie","scatter","function","radar","candlestick","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 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("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("candlestick"===s){needColumn(u.x,"encoding.x",!0);const n=toArray(u.y);if(4!==n.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${n.length} 列)。`,"encoding.y");else for(const e of n)needNumeric(e,"encoding.y",!0);return 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("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"candlestick":return function compileCandlestick(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),[o,r,a,s]=toArray(t.y).map(n=>columnIndex(e,n)),l=[],c=[];for(const n of e.rows){const e=toLabel(n[i]),t=toNumber(n[o]),u=toNumber(n[r]),m=toNumber(n[a]),d=toNumber(n[s]);e&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(e)||l.push(e),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(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,c=[],pushX=n=>{c.some(e=>e===n)||c.push(n)};let u;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]))}u=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:c.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else u=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:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:c},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","candlestick","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;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{diagnoseExpression as e,evaluateExpression as n,createChart as t}from"@damoqiongqiu/ice-chart";const i=1,r=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"],s=["line","area","bar","scatter","candlestick","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","function"];function resolveDataset(e){const n=e&&e.data;if(!n)return null;if(Array.isArray(n)){const e=n.filter(e=>e&&"object"==typeof e&&!Array.isArray(e));if(!e.length)return{columns:[],rows:[]};const t=[];for(const n of e)for(const e of Object.keys(n))t.includes(e)||t.push(e);return{columns:t,rows:e.map(e=>t.map(n=>e[n]))}}if("object"!=typeof n)return null;const t=Array.isArray(n.columns)?n.columns.map(e=>String(e)):[],i=Array.isArray(n.rows)?n.rows:[];return t.length?{columns:t,rows:i.map(e=>Array.isArray(e)?e:[])}:null}function columnIndex(e,n){return e&&n?e.columns.indexOf(n):-1}function isNumericCell(e){return"number"==typeof e?isFinite(e):"string"==typeof e&&""!==e.trim()&&isFinite(Number(e))}function numericRatio(e,n){let t=0,i=0;for(const r of e.rows){const e=r[n];null!=e&&""!==e&&(t+=1,isNumericCell(e)&&(i+=1))}return t?i/t:0}function toNumber(e){if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e&&""!==e.trim()){const n=Number(e);return isFinite(n)?n:null}return null}function toLabel(e){return null==e?"":String(e)}function toArray(e){return null==e?[]:Array.isArray(e)?e.filter(e=>"string"==typeof e&&e):[e]}function firstColumn(e){return Array.isArray(e)?e[0]:e}const o=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],a=["x","y","series","size","color","name","value","total","source","target"];function validateChartDsl(t){const i=[],s=[],fail=(e,n,t)=>i.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>s.push({severity:"warning",code:e,message:n,path:t});if(!t||"object"!=typeof t||Array.isArray(t))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(i,s);void 0!==t.schemaVersion&&1!==t.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${t.schemaVersion}(当前是 1)。`,"schemaVersion"),t.kind?r.includes(t.kind)||fail("unsupported-kind",`不支持的 kind「${t.kind}」。可用类型:${r.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${r.join(" / ")}。`,"kind");for(const e of Object.keys(t))o.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const l=t.kind,c=Array.isArray(t.series)&&t.series.length>0;if(c&&t.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===l||"parametric"===l){const r=String(t.expression||"");if(r.trim()){const o="function"===l?"x":"t",a=t.params&&"object"==typeof t.params?t.params:{},c=function sampleExpression(e,t,i,r,s){const o="function"===s?[-10,10]:[0,2*Math.PI],a=Array.isArray(r)&&isFinite(Number(r[0]))?Number(r[0]):o[0],l=Array.isArray(r)&&isFinite(Number(r[1]))?Number(r[1]):o[1];if(!(l>a))return[];const c=[],u=64;try{for(let r=0;r<=u;r++){const s=a+(l-a)*r/u,o={...i,[t]:s},m=n(e,o);c.push("number"==typeof m?m:NaN)}}catch(e){return[]}return c}(r,o,a,t.domain,l);for(const n of e(r,{variable:o,params:a,values:c})){const e={severity:n.severity,code:`expression-${n.code}`,message:n.message,path:"expression"};("error"===n.severity?i:s).push(e)}}else fail("missing-expression",`kind「${l}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(i,s)}if(c)return finish(i,s);const u=resolveDataset(t);if(void 0===t.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(i,s);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(i,s);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(i,s);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(i,s);u.rows.forEach((e,n)=>{e.length!==u.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${n}]`)});const m=t.encoding&&"object"==typeof t.encoding?t.encoding:{};t.encoding||fail("missing-encoding",`kind「${l}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const e of Object.keys(m))a.includes(e)||warn("unknown-encoding",`encoding 里的未知通道「${e}」会被忽略。`,`encoding.${e}`);const needColumn=(e,n,t)=>{if(!e)return t&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${u.columns.join(" / ")}。`,n),-1;const i=columnIndex(u,e);return i<0?(fail("unknown-column",`列「${e}」不存在。可用列:${u.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(u,i)?i:(fail("empty-column",`列「${e}」整列都是空值。`,n),-1)},needNumeric=(e,n,t)=>{const i=needColumn(e,n,t);if(i<0)return-1;const r=numericRatio(u,i);return r<.6?(fail("non-numeric-column",`列「${e}」不是数值列(数字占比 ${(100*r).toFixed(0)}%),不能当 ${n.split(".")[1]} 用。`,n),-1):(r<1&&warn("partly-non-numeric",`列「${e}」有 ${Math.round(100*(1-r))}% 的值不是数字,这些点会被当作空缺。`,n),i)};if("pie"===l||"funnel"===l||"gauge"===l||"liquid"===l){needColumn(m.name,"encoding.name","pie"===l||"funnel"===l);const e=needNumeric(m.value,"encoding.value",!0);if(e>=0&&"pie"===l){const n=u.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return e>=0&&("gauge"===l||"liquid"===l)&&u.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(i,s)}if("radar"===l){const e=needColumn(m.x,"encoding.x",!0),n=needNumeric(toArray(m.y)[0],"encoding.y",!0),t=m.series?needColumn(m.series,"encoding.series",!1):-1;if(toArray(m.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),e>=0&&n>=0){const i=new Set(u.rows.map(n=>String(n[e]))),r=new Set(u.rows.map(e=>t>=0?String(e[t]):"—")),s=u.rows.filter(e=>isFinite(Number(e[n]))).length,o=i.size*r.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),s<o&&warn("radar-missing-value",`有 ${o-s} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(i,s)}if("heatmap"===l)return needColumn(m.x,"encoding.x",!0),needColumn(m.y,"encoding.y",!0),needNumeric(m.value||m.color,m.value?"encoding.value":"encoding.color",!0),finish(i,s);if("candlestick"===l){needColumn(m.x,"encoding.x",!0);const e=toArray(m.y);if(4!==e.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${e.length} 列)。`,"encoding.y");else for(const n of e)needNumeric(n,"encoding.y",!0);return finish(i,s)}if("waterfall"===l)return needColumn(m.name,"encoding.name",!0),needNumeric(m.value,"encoding.value",!0),m.total&&needColumn(m.total,"encoding.total",!1),finish(i,s);if("sankey"===l){needColumn(m.source,"encoding.source",!0),needColumn(m.target,"encoding.target",!0);const e=needNumeric(m.value,"encoding.value",!0);if(e>=0){const n=u.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<=0}).length;n&&warn("sankey-non-positive",`${n} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(i,s)}if("line"===l||"area"===l||"bar"===l||"scatter"===l){"scatter"===l&&m.size&&needNumeric(m.size,"encoding.size",!1);const e=toArray(m.y);e.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),e.length>8&&warn("too-many-series",`y 绑定了 ${e.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(m.x,"encoding.x",!0);for(const n of e)needNumeric(n,"encoding.y",!0);return m.series&&needColumn(m.series,"encoding.series",!1),finish(i,s)}return warn("passthrough-kind",`kind「${l}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(i,s)}function finish(e,n){return{valid:0===e.length,errors:e,warnings:n}}function formatDiagnostics(e){const n=[...e.errors,...e.warnings].map(e=>{const n="error"===e.severity?"错误":"警告",t=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${t}`});return n.length?n.join("\n"):"✓ DSL 校验通过"}class ChartDslCompileError extends Error{constructor(e){super(`[ice-chart-dsl] DSL 校验不通过:\n${e.map(e=>`- ${e.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=e}}function compileChartDsl(e){const n=validateChartDsl(e);if(!n.valid)throw new ChartDslCompileError(n.errors);const t=e.kind,i={title:normalizeTitle(e),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(e.series)&&e.series.length)i.series=e.series;else if("function"===t||"parametric"===t)i.legend={show:!1},i.tooltip={trigger:"item"},i.crosshair={show:!0,axis:"x",showAxisLabel:!0},i.xAxis={type:"value",name:"x"},i.yAxis={name:"y"},i.series=[{id:"curve",type:t,name:e.title&&"object"==typeof e.title?e.title.text:String(e.title||("function"===t?"f(x)":"curve")),expression:e.expression,domain:e.domain,params:e.params,lineWidth:2}];else{const n=resolveDataset(e);Object.assign(i,function compileByKind(e,n,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.name),s=columnIndex(n,i.value),o=n.rows.map(e=>({name:toLabel(e[r]),value:toNumber(e[s])})).filter(e=>""!==e.name&&isFinite(e.value)),a="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:a?o.slice(0,1):o}]}}(e,n,t);case"radar":return function compileRadar(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.series),o=[],a=new Map;for(const e of n.rows){const n=toLabel(e[i]),l=toNumber(e[r]);if(!n||null===l)continue;o.includes(n)||o.push(n);const c=s>=0?toLabel(e[s])||"(空)":String(firstColumn(t.y));a.has(c)||a.set(c,new Map),a.get(c).set(n,l)}const l=o.map(e=>{let n=0;for(const t of a.values())n=Math.max(n,Math.abs(t.get(e)??0));return function niceCeil(e){if(!isFinite(e)||e<=0)return 1;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<=1?1:t<=2?2:t<=5?5:10)*n}(n)});return{legend:{show:a.size>1},tooltip:{trigger:"item"},radar:{indicators:o.map((e,n)=>({name:e,max:l[n]})),splitNumber:4},series:[...a.entries()].map(([e,n],t)=>({id:`radar-${t+1}`,type:"radar",name:e,data:o.map(e=>n.get(e)??0)}))}}(e,n);case"heatmap":return function compileHeatmap(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),s=columnIndex(n,t.value||t.color),o=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[s]);n&&t&&null!==l&&(o.includes(n)||o.push(n),a.push([n,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:o},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:a}]}}(e,n);case"candlestick":return function compileCandlestick(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),[r,s,o,a]=toArray(t.y).map(e=>columnIndex(n,e)),l=[],c=[];for(const e of n.rows){const n=toLabel(e[i]),t=toNumber(e[r]),u=toNumber(e[s]),m=toNumber(e[o]),d=toNumber(e[a]);n&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(n)||l.push(n),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(e,n);case"waterfall":return function compileWaterfall(e,n){const t=e.encoding||{},i=columnIndex(n,t.name),r=columnIndex(n,t.value),s=columnIndex(n,t.total),o=n.rows.map(e=>{const n=toLabel(e[i]),t=toNumber(e[r]),o=s>=0?e[s]:void 0;return null!=o&&""!==o&&!1!==o&&0!==Number(o)?{name:n,value:isFinite(t)?t:0,total:!0}:{name:n,value:t}}).filter(e=>""!==e.name&&(null===e.value||isFinite(e.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:o}]}}(e,n);case"sankey":return function compileSankey(e,n){const t=e.encoding||{},i=columnIndex(n,t.source),r=columnIndex(n,t.target),s=columnIndex(n,t.value),o=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[s]);!n||!t||null===l||l<=0||(o.some(e=>e.name===n)||o.push({name:n}),o.some(e=>e.name===t)||o.push({name:t}),a.push({source:n,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:o,links:a,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(e,n);default:return function compileCartesian(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.x),s=toArray(i.y),o=r>=0&&numericRatio(n,r)>.9,a=i.size?columnIndex(n,i.size):-1,l=i.series?columnIndex(n,i.series):-1,c=[],pushX=e=>{c.some(n=>n===e)||c.push(e)};let u;if(l>=0){const e=new Map,i=columnIndex(n,s[0]);for(const t of n.rows){const n=o?toNumber(t[r]):toLabel(t[r]);if(""===n||null===n)continue;pushX(n);const s=toLabel(t[l])||"(空)";e.has(s)||e.set(s,new Map),e.get(s).set(n,toNumber(t[i]))}u=[...e.entries()].map(([e,n],i)=>({id:`series-${i+1}`,type:t,name:e,data:c.map(e=>o?[e,n.get(e)??null]:n.get(e)??null)}))}else u=s.map((e,i)=>{const s=columnIndex(n,e),l=n.rows.map(e=>{const n=o?toNumber(e[r]):toLabel(e[r]),t=toNumber(e[s]);if(""===n||null===n||null===t)return null;if(pushX(n),a>=0){const i=toNumber(e[a]);return[n,t,null===i?8:i]}return o?[n,t]:t}).filter(e=>null!==e);return{id:`series-${i+1}`,type:t,name:e,data:l}});const m={legend:{show:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=o?{type:"value",name:i.x}:{type:"category",name:i.x,data:c},m.yAxis={name:1===s.length?s[0]:void 0},m}(e,n,t)}}(e,n,t))}if(e.options&&"object"==typeof e.options)for(const n of Object.keys(e.options)){const t=e.options[n];void 0!==t&&("series"!==n&&(i[n]=t))}return i}function normalizeTitle(e){if(e.title)return"string"==typeof e.title?{text:e.title}:e.title}function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))}function renderChartDsl(e,n,i){const r=validateChartDsl(n),s=compileChartDsl(n);return{chart:t(e,s,i),option:s,diagnostics:r}}export{s as CHART_DSL_COMPILED_KINDS,r as CHART_DSL_KINDS,i as CHART_DSL_SCHEMA_VERSION,ChartDslCompileError,chartDslToJsonString,columnIndex,compileChartDsl,formatDiagnostics,numericRatio,renderChartDsl,resolveDataset,validateChartDsl};
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","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"],r=["line","area","bar","scatter","candlestick","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,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("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("candlestick"===l){needColumn(m.x,"encoding.x",!0);const n=toArray(m.y);if(4!==n.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${n.length} 列)。`,"encoding.y");else for(const e of n)needNumeric(e,"encoding.y",!0);return 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("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"candlestick":return function compileCandlestick(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),[o,r,a,s]=toArray(t.y).map(n=>columnIndex(e,n)),l=[],c=[];for(const n of e.rows){const e=toLabel(n[i]),t=toNumber(n[o]),u=toNumber(n[r]),m=toNumber(n[a]),d=toNumber(n[s]);e&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(e)||l.push(e),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(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,c=[],pushX=n=>{c.some(e=>e===n)||c.push(n)};let u;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]))}u=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:c.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else u=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:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:c},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};
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("@damoqiongqiu/ice-chart")):"function"==typeof define&&define.amd?define(["exports","@damoqiongqiu/ice-chart"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ICEChartDSL={},e.ICEChart)}(this,function(e,n){"use strict";const t=["line","area","bar","pie","scatter","function","radar","candlestick","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(e){const n=e&&e.data;if(!n)return null;if(Array.isArray(n)){const e=n.filter(e=>e&&"object"==typeof e&&!Array.isArray(e));if(!e.length)return{columns:[],rows:[]};const t=[];for(const n of e)for(const e of Object.keys(n))t.includes(e)||t.push(e);return{columns:t,rows:e.map(e=>t.map(n=>e[n]))}}if("object"!=typeof n)return null;const t=Array.isArray(n.columns)?n.columns.map(e=>String(e)):[],i=Array.isArray(n.rows)?n.rows:[];return t.length?{columns:t,rows:i.map(e=>Array.isArray(e)?e:[])}:null}function columnIndex(e,n){return e&&n?e.columns.indexOf(n):-1}function isNumericCell(e){return"number"==typeof e?isFinite(e):"string"==typeof e&&""!==e.trim()&&isFinite(Number(e))}function numericRatio(e,n){let t=0,i=0;for(const r of e.rows){const e=r[n];null!=e&&""!==e&&(t+=1,isNumericCell(e)&&(i+=1))}return t?i/t:0}function toNumber(e){if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e&&""!==e.trim()){const n=Number(e);return isFinite(n)?n:null}return null}function toLabel(e){return null==e?"":String(e)}function toArray(e){return null==e?[]:Array.isArray(e)?e.filter(e=>"string"==typeof e&&e):[e]}function firstColumn(e){return Array.isArray(e)?e[0]:e}const i=["schemaVersion","kind","data","encoding","title","expression","domain","params","options","series"],r=["x","y","series","size","color","name","value","total","source","target"];function validateChartDsl(e){const o=[],s=[],fail=(e,n,t)=>o.push({severity:"error",code:e,message:n,path:t}),warn=(e,n,t)=>s.push({severity:"warning",code:e,message:n,path:t});if(!e||"object"!=typeof e||Array.isArray(e))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(o,s);void 0!==e.schemaVersion&&1!==e.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${e.schemaVersion}(当前是 1)。`,"schemaVersion"),e.kind?t.includes(e.kind)||fail("unsupported-kind",`不支持的 kind「${e.kind}」。可用类型:${t.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${t.join(" / ")}。`,"kind");for(const n of Object.keys(e))i.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const a=e.kind,l=Array.isArray(e.series)&&e.series.length>0;if(l&&e.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===a||"parametric"===a){const t=String(e.expression||"");if(t.trim()){const i="function"===a?"x":"t",r=e.params&&"object"==typeof e.params?e.params:{},l=function sampleExpression(e,t,i,r,o){const s="function"===o?[-10,10]:[0,2*Math.PI],a=Array.isArray(r)&&isFinite(Number(r[0]))?Number(r[0]):s[0],l=Array.isArray(r)&&isFinite(Number(r[1]))?Number(r[1]):s[1];if(!(l>a))return[];const c=[],u=64;try{for(let r=0;r<=u;r++){const o=a+(l-a)*r/u,s={...i,[t]:o},m=n.evaluateExpression(e,s);c.push("number"==typeof m?m:NaN)}}catch(e){return[]}return c}(t,i,r,e.domain,a);for(const e of n.diagnoseExpression(t,{variable:i,params:r,values:l})){const n={severity:e.severity,code:`expression-${e.code}`,message:e.message,path:"expression"};("error"===e.severity?o:s).push(n)}}else fail("missing-expression",`kind「${a}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(o,s)}if(l)return finish(o,s);const c=resolveDataset(e);if(void 0===e.data)return fail("missing-data",`kind「${a}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(o,s);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(o,s);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(o,s);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(o,s);c.rows.forEach((e,n)=>{e.length!==c.columns.length&&fail("row-length-mismatch",`第 ${n+1} 行有 ${e.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${n}]`)});const u=e.encoding&&"object"==typeof e.encoding?e.encoding:{};e.encoding||fail("missing-encoding",`kind「${a}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.columns[0]}" }。`,"encoding");for(const e of Object.keys(u))r.includes(e)||warn("unknown-encoding",`encoding 里的未知通道「${e}」会被忽略。`,`encoding.${e}`);const needColumn=(e,n,t)=>{if(!e)return t&&fail("missing-encoding-channel",`${n} 是必填的。可用列:${c.columns.join(" / ")}。`,n),-1;const i=columnIndex(c,e);return i<0?(fail("unknown-column",`列「${e}」不存在。可用列:${c.columns.join(" / ")}。`,n),-1):function hasValue(e,n){return e.rows.some(e=>{const t=e[n];return null!=t&&""!==t})}(c,i)?i:(fail("empty-column",`列「${e}」整列都是空值。`,n),-1)},needNumeric=(e,n,t)=>{const i=needColumn(e,n,t);if(i<0)return-1;const r=numericRatio(c,i);return r<.6?(fail("non-numeric-column",`列「${e}」不是数值列(数字占比 ${(100*r).toFixed(0)}%),不能当 ${n.split(".")[1]} 用。`,n),-1):(r<1&&warn("partly-non-numeric",`列「${e}」有 ${Math.round(100*(1-r))}% 的值不是数字,这些点会被当作空缺。`,n),i)};if("pie"===a||"funnel"===a||"gauge"===a||"liquid"===a){needColumn(u.name,"encoding.name","pie"===a||"funnel"===a);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0&&"pie"===a){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<0});n.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${n.length} 行负数。`,"encoding.value")}return e>=0&&("gauge"===a||"liquid"===a)&&c.rows.length>1&&warn("single-value-kind",`kind「${a}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(o,s)}if("radar"===a){const e=needColumn(u.x,"encoding.x",!0),n=needNumeric(toArray(u.y)[0],"encoding.y",!0),t=u.series?needColumn(u.series,"encoding.series",!1):-1;if(toArray(u.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),e>=0&&n>=0){const i=new Set(c.rows.map(n=>String(n[e]))),r=new Set(c.rows.map(e=>t>=0?String(e[t]):"—")),o=c.rows.filter(e=>isFinite(Number(e[n]))).length,s=i.size*r.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),o<s&&warn("radar-missing-value",`有 ${s-o} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(o,s)}if("heatmap"===a)return needColumn(u.x,"encoding.x",!0),needColumn(u.y,"encoding.y",!0),needNumeric(u.value||u.color,u.value?"encoding.value":"encoding.color",!0),finish(o,s);if("candlestick"===a){needColumn(u.x,"encoding.x",!0);const e=toArray(u.y);if(4!==e.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${e.length} 列)。`,"encoding.y");else for(const n of e)needNumeric(n,"encoding.y",!0);return finish(o,s)}if("waterfall"===a)return needColumn(u.name,"encoding.name",!0),needNumeric(u.value,"encoding.value",!0),u.total&&needColumn(u.total,"encoding.total",!1),finish(o,s);if("sankey"===a){needColumn(u.source,"encoding.source",!0),needColumn(u.target,"encoding.target",!0);const e=needNumeric(u.value,"encoding.value",!0);if(e>=0){const n=c.rows.filter(n=>{const t=Number(n[e]);return isFinite(t)&&t<=0}).length;n&&warn("sankey-non-positive",`${n} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(o,s)}if("line"===a||"area"===a||"bar"===a||"scatter"===a){"scatter"===a&&u.size&&needNumeric(u.size,"encoding.size",!1);const e=toArray(u.y);e.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),e.length>8&&warn("too-many-series",`y 绑定了 ${e.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(u.x,"encoding.x",!0);for(const n of e)needNumeric(n,"encoding.y",!0);return u.series&&needColumn(u.series,"encoding.series",!1),finish(o,s)}return warn("passthrough-kind",`kind「${a}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(o,s)}function finish(e,n){return{valid:0===e.length,errors:e,warnings:n}}class ChartDslCompileError extends Error{constructor(e){super(`[ice-chart-dsl] DSL 校验不通过:\n${e.map(e=>`- ${e.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=e}}function compileChartDsl(e){const n=validateChartDsl(e);if(!n.valid)throw new ChartDslCompileError(n.errors);const t=e.kind,i={title:normalizeTitle(e),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(e.series)&&e.series.length)i.series=e.series;else if("function"===t||"parametric"===t)i.legend={show:!1},i.tooltip={trigger:"item"},i.crosshair={show:!0,axis:"x",showAxisLabel:!0},i.xAxis={type:"value",name:"x"},i.yAxis={name:"y"},i.series=[{id:"curve",type:t,name:e.title&&"object"==typeof e.title?e.title.text:String(e.title||("function"===t?"f(x)":"curve")),expression:e.expression,domain:e.domain,params:e.params,lineWidth:2}];else{const n=resolveDataset(e);Object.assign(i,function compileByKind(e,n,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.name),o=columnIndex(n,i.value),s=n.rows.map(e=>({name:toLabel(e[r]),value:toNumber(e[o])})).filter(e=>""!==e.name&&isFinite(e.value)),a="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:a?s.slice(0,1):s}]}}(e,n,t);case"radar":return function compileRadar(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),o=columnIndex(n,t.series),s=[],a=new Map;for(const e of n.rows){const n=toLabel(e[i]),l=toNumber(e[r]);if(!n||null===l)continue;s.includes(n)||s.push(n);const c=o>=0?toLabel(e[o])||"(空)":String(firstColumn(t.y));a.has(c)||a.set(c,new Map),a.get(c).set(n,l)}const l=s.map(e=>{let n=0;for(const t of a.values())n=Math.max(n,Math.abs(t.get(e)??0));return function niceCeil(e){if(!isFinite(e)||e<=0)return 1;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<=1?1:t<=2?2:t<=5?5:10)*n}(n)});return{legend:{show:a.size>1},tooltip:{trigger:"item"},radar:{indicators:s.map((e,n)=>({name:e,max:l[n]})),splitNumber:4},series:[...a.entries()].map(([e,n],t)=>({id:`radar-${t+1}`,type:"radar",name:e,data:s.map(e=>n.get(e)??0)}))}}(e,n);case"heatmap":return function compileHeatmap(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),r=columnIndex(n,firstColumn(t.y)),o=columnIndex(n,t.value||t.color),s=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[o]);n&&t&&null!==l&&(s.includes(n)||s.push(n),a.push([n,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:s},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:a}]}}(e,n);case"candlestick":return function compileCandlestick(e,n){const t=e.encoding||{},i=columnIndex(n,t.x),[r,o,s,a]=toArray(t.y).map(e=>columnIndex(n,e)),l=[],c=[];for(const e of n.rows){const n=toLabel(e[i]),t=toNumber(e[r]),u=toNumber(e[o]),m=toNumber(e[s]),d=toNumber(e[a]);n&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(n)||l.push(n),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(e,n);case"waterfall":return function compileWaterfall(e,n){const t=e.encoding||{},i=columnIndex(n,t.name),r=columnIndex(n,t.value),o=columnIndex(n,t.total),s=n.rows.map(e=>{const n=toLabel(e[i]),t=toNumber(e[r]),s=o>=0?e[o]:void 0;return null!=s&&""!==s&&!1!==s&&0!==Number(s)?{name:n,value:isFinite(t)?t:0,total:!0}:{name:n,value:t}}).filter(e=>""!==e.name&&(null===e.value||isFinite(e.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:s}]}}(e,n);case"sankey":return function compileSankey(e,n){const t=e.encoding||{},i=columnIndex(n,t.source),r=columnIndex(n,t.target),o=columnIndex(n,t.value),s=[],a=[];for(const e of n.rows){const n=toLabel(e[i]),t=toLabel(e[r]),l=toNumber(e[o]);!n||!t||null===l||l<=0||(s.some(e=>e.name===n)||s.push({name:n}),s.some(e=>e.name===t)||s.push({name:t}),a.push({source:n,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:s,links:a,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(e,n);default:return function compileCartesian(e,n,t){const i=e.encoding||{},r=columnIndex(n,i.x),o=toArray(i.y),s=r>=0&&numericRatio(n,r)>.9,a=i.size?columnIndex(n,i.size):-1,l=i.series?columnIndex(n,i.series):-1,c=[],pushX=e=>{c.some(n=>n===e)||c.push(e)};let u;if(l>=0){const e=new Map,i=columnIndex(n,o[0]);for(const t of n.rows){const n=s?toNumber(t[r]):toLabel(t[r]);if(""===n||null===n)continue;pushX(n);const o=toLabel(t[l])||"(空)";e.has(o)||e.set(o,new Map),e.get(o).set(n,toNumber(t[i]))}u=[...e.entries()].map(([e,n],i)=>({id:`series-${i+1}`,type:t,name:e,data:c.map(e=>s?[e,n.get(e)??null]:n.get(e)??null)}))}else u=o.map((e,i)=>{const o=columnIndex(n,e),l=n.rows.map(e=>{const n=s?toNumber(e[r]):toLabel(e[r]),t=toNumber(e[o]);if(""===n||null===n||null===t)return null;if(pushX(n),a>=0){const i=toNumber(e[a]);return[n,t,null===i?8:i]}return s?[n,t]:t}).filter(e=>null!==e);return{id:`series-${i+1}`,type:t,name:e,data:l}});const m={legend:{show:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=s?{type:"value",name:i.x}:{type:"category",name:i.x,data:c},m.yAxis={name:1===o.length?o[0]:void 0},m}(e,n,t)}}(e,n,t))}if(e.options&&"object"==typeof e.options)for(const n of Object.keys(e.options)){const t=e.options[n];void 0!==t&&("series"!==n&&(i[n]=t))}return i}function normalizeTitle(e){if(e.title)return"string"==typeof e.title?{text:e.title}:e.title}e.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","candlestick","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","function"],e.CHART_DSL_KINDS=t,e.CHART_DSL_SCHEMA_VERSION=1,e.ChartDslCompileError=ChartDslCompileError,e.chartDslToJsonString=function chartDslToJsonString(e){return JSON.stringify(compileChartDsl(e))},e.columnIndex=columnIndex,e.compileChartDsl=compileChartDsl,e.formatDiagnostics=function formatDiagnostics(e){const n=[...e.errors,...e.warnings].map(e=>{const n="error"===e.severity?"错误":"警告",t=e.path?`(${e.path})`:"";return`[${n}] ${e.message}${t}`});return n.length?n.join("\n"):"✓ DSL 校验通过"},e.numericRatio=numericRatio,e.renderChartDsl=function renderChartDsl(e,t,i){const r=validateChartDsl(t),o=compileChartDsl(t);return{chart:n.createChart(e,o,i),option:o,diagnostics:r}},e.resolveDataset=resolveDataset,e.validateChartDsl=validateChartDsl});
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","candlestick","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 c=[],u=64;try{for(let o=0;o<=u;o++){const a=s+(l-s)*o/u,r={...i,[t]:a},m=e.evaluateExpression(n,r);c.push("number"==typeof m?m:NaN)}}catch(n){return[]}return c}(t,i,o,n.domain,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 c=resolveDataset(n);if(void 0===n.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(a,r);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(a,r);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(a,r);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(a,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 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("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(a,r)}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]):"—")),a=c.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(u.x,"encoding.x",!0),needColumn(u.y,"encoding.y",!0),needNumeric(u.value||u.color,u.value?"encoding.value":"encoding.color",!0),finish(a,r);if("candlestick"===s){needColumn(u.x,"encoding.x",!0);const n=toArray(u.y);if(4!==n.length)fail("ohlc-needs-four-columns",`K 线的 encoding.y 必须是四列,按 [开, 收, 低, 高] 顺序给出(当前 ${n.length} 列)。`,"encoding.y");else for(const e of n)needNumeric(e,"encoding.y",!0);return finish(a,r)}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(a,r);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(a,r)}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(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 c=a>=0?toLabel(n[a])||"(空)":String(firstColumn(t.y));s.has(c)||s.set(c,new Map),s.get(c).set(e,l)}const l=r.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:r.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:r.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.value||t.color),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);e&&t&&null!==l&&(r.includes(e)||r.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:r},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"candlestick":return function compileCandlestick(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),[o,a,r,s]=toArray(t.y).map(n=>columnIndex(e,n)),l=[],c=[];for(const n of e.rows){const e=toLabel(n[i]),t=toNumber(n[o]),u=toNumber(n[a]),m=toNumber(n[r]),d=toNumber(n[s]);e&&null!==t&&null!==u&&null!==m&&null!==d&&(l.includes(e)||l.push(e),c.push([t,u,m,d]))}return{legend:{show:!1},tooltip:{trigger:"item"},crosshair:{show:!0,axis:"x",showAxisLabel:!0},xAxis:{type:"category",name:t.x,data:l},yAxis:{name:"OHLC"},series:[{id:"candlestick",type:"candlestick",name:t.x,data:c}]}}(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,c=[],pushX=n=>{c.some(e=>e===n)||c.push(n)};let u;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]))}u=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:c.map(n=>r?[n,e.get(n)??null]:e.get(n)??null)}))}else u=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:u.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:u};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=r?{type:"value",name:i.x}:{type:"category",name:i.x,data:c},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","candlestick","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});
@@ -47,11 +47,66 @@ export interface ChartDslEncoding {
47
47
  /** 桑基图:连线终点列。 */
48
48
  target?: string;
49
49
  }
50
+ /** 标注的定位轴:`'y'`(默认)画水平线,`'x'` 画垂直线。 */
51
+ export type ChartDslAnnotationAxis = 'x' | 'y';
52
+ /**
53
+ * 标注线:目标线 / 阈值线 / 告警线。
54
+ *
55
+ * `value` 是**数据值**,不是像素:数值轴写数字,类目轴写类目名(或下标)。
56
+ * 定位由 ice-chart 按坐标轴比例尺算,因此缩放 / 平移后线会跟着走。
57
+ */
58
+ export interface ChartDslAnnotationLine {
59
+ axis?: ChartDslAnnotationAxis;
60
+ value: number | string;
61
+ text?: string;
62
+ color?: string;
63
+ /** 是否虚线,默认 true。 */
64
+ dashed?: boolean;
65
+ /** 文字沿线的位置,默认 `'end'`。 */
66
+ textPosition?: 'start' | 'center' | 'end';
67
+ }
68
+ /** 标注点:异常点 / 事件点。`x` 与 `y` 都是数据值。 */
69
+ export interface ChartDslAnnotationPoint {
70
+ x: number | string;
71
+ y: number;
72
+ text?: string;
73
+ color?: string;
74
+ symbol?: 'circle' | 'rect' | 'diamond' | 'triangle';
75
+ textPosition?: 'top' | 'bottom' | 'left' | 'right';
76
+ }
77
+ /** 标注区间:达标区 / 维护窗口。`from` / `to` 是同一根轴上的两个数据值。 */
78
+ export interface ChartDslAnnotationArea {
79
+ axis?: ChartDslAnnotationAxis;
80
+ from: number | string;
81
+ to: number | string;
82
+ color?: string;
83
+ text?: string;
84
+ textPosition?: 'start' | 'center' | 'end';
85
+ }
86
+ /**
87
+ * 标注图层(目标线 / 阈值线 / 异常点 / 目标区间)。
88
+ *
89
+ * 它是**挂在坐标系上的一个图层**,不是新的图表类型:不进图例、不占数据下标,
90
+ * 也不参与命中测试。越界的标注不会被画出来(ice-chart 会在
91
+ * `chart.annotationDiagnostics()` 里给出原因,本包在编译期就把明显写错的挡下来)。
92
+ */
93
+ export interface ChartDslAnnotation {
94
+ lines?: ChartDslAnnotationLine[];
95
+ points?: ChartDslAnnotationPoint[];
96
+ areas?: ChartDslAnnotationArea[];
97
+ }
50
98
  export interface ChartDslDocument {
51
99
  schemaVersion?: number;
52
100
  kind: ChartDslKind;
53
101
  data?: ChartDslDataset;
54
102
  encoding?: ChartDslEncoding;
103
+ /**
104
+ * 标注图层:目标线 / 阈值线 / 异常点 / 目标区间。
105
+ *
106
+ * 只对**直角坐标**的 kind 有意义(line / area / bar / scatter);给饼图 / 雷达 / 桑基等
107
+ * 会被警告(那些场景没有 x/y 坐标系,标准无处可放)。
108
+ */
109
+ annotation?: ChartDslAnnotation;
55
110
  /** 标题:字符串等价于 `{ text }`。 */
56
111
  title?: string | {
57
112
  text?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damoqiongqiu/ice-chart-dsl",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "author": "大漠穷秋",
6
6
  "description": "A JSON-first DSL layer for AI agents to build charts with ice-chart.",
@@ -44,14 +44,14 @@
44
44
  "prepublishOnly": "npm run verify"
45
45
  },
46
46
  "peerDependencies": {
47
- "@damoqiongqiu/ice-chart": "^0.19.0",
47
+ "@damoqiongqiu/ice-chart": "^0.20.0",
48
48
  "ice-render": "^2.3.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@babel/core": "7.28.5",
52
52
  "@babel/preset-env": "7.28.5",
53
53
  "@babel/preset-typescript": "7.28.5",
54
- "@damoqiongqiu/ice-chart": "^0.19.0",
54
+ "@damoqiongqiu/ice-chart": "^0.20.0",
55
55
  "@rollup/plugin-babel": "6.1.0",
56
56
  "@rollup/plugin-commonjs": "28.0.1",
57
57
  "@rollup/plugin-json": "6.1.0",
@@ -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.1.3"
4
+ version: "0.2.0"
5
5
  category: ux
6
6
  platforms:
7
7
  - claude-code
@@ -73,6 +73,39 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
73
73
  - `encoding.source` + `encoding.target` + `encoding.value` make a `sankey`.
74
74
  - `kind: "function"` needs `expression` (+ optional `domain`, `params`) and no data.
75
75
 
76
+ ## Annotations (goal lines / thresholds / target bands)
77
+
78
+ Business charts almost always need a **reference layer** on top of the series — a goal line,
79
+ an SLA threshold, an event marker, a target band. That is one field, not a new chart type:
80
+
81
+ ```json
82
+ {
83
+ "kind": "line",
84
+ "data": { "columns": ["月份", "销量"], "rows": [["1月", 120], ["2月", 132]] },
85
+ "encoding": { "x": "月份", "y": "销量" },
86
+ "annotation": {
87
+ "lines": [
88
+ { "axis": "y", "value": 150, "text": "目标 150" },
89
+ { "axis": "y", "value": 100, "text": "告警阈值", "color": "#dc3545" },
90
+ { "axis": "x", "value": "2月", "text": "上线" }
91
+ ],
92
+ "points": [{ "x": "2月", "y": 132, "text": "异常点", "symbol": "diamond" }],
93
+ "areas": [{ "axis": "y", "from": 0, "to": 100, "text": "达标区" }]
94
+ }
95
+ }
96
+ ```
97
+
98
+ - `value` / `from` / `to` / `x` / `y` are **data values**, not pixels: numbers on a numeric axis,
99
+ category names (or indexes) on a category axis, timestamps or date strings on a time axis.
100
+ - Annotations live on the coordinate system, so they follow zoom / pan, stay out of the legend,
101
+ do not occupy data indexes, and never steal hit-testing from the series.
102
+ - Only cartesian kinds (`line` / `area` / `bar` / `scatter`) can host annotations; using them on
103
+ `pie` / `radar` / `sankey` / … raises `annotation-non-cartesian` (those scenes have no x/y axes).
104
+ - Structural mistakes are **errors** (`missing-annotation-value`: missing `value`, or `from` without
105
+ `to`); suspicious values are **warnings** (`annotation-unknown-category`,
106
+ `annotation-value-type`, `annotation-empty-area`). Out-of-range values are simply not drawn —
107
+ `chart.annotationDiagnostics()` gives the reason at runtime.
108
+
76
109
  ## API
77
110
 
78
111
  ```ts
@@ -102,6 +135,6 @@ Example feedback:
102
135
  ## Reference
103
136
 
104
137
  - package: `@damoqiongqiu/ice-chart-dsl` (npm)
105
- - runtime peer dependencies: `@damoqiongqiu/ice-chart@^0.19.0` +
138
+ - runtime peer dependencies: `@damoqiongqiu/ice-chart@^0.20.0` (标注需要 0.20.0 起)+
106
139
  `ice-render@^2.3.0`(家族当前引擎 `2.3.0`,建议直接装最新)
107
140
  - schema: `src/schema/chart-dsl.schema.json`