@damoqiongqiu/ice-chart-dsl 0.5.4 → 0.6.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 +0 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/types/types.d.ts +1 -1
- package/package.json +5 -5
- package/skills/ice-chart-dsl/SKILL.md +1 -2
package/README.md
CHANGED
|
@@ -81,7 +81,6 @@ if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));
|
|
|
81
81
|
| `pie` | `name` + `value` | 负值会被警告(饼图不表达负值) |
|
|
82
82
|
| `radar` | `x`(指标)+ `y`(数值)+ `series` | 指标名从 x 列推,上限自动取整到好看的刻度 |
|
|
83
83
|
| `heatmap` | `x` + `y`(两个类目列)+ `value` | 二维矩阵表直接画 |
|
|
84
|
-
| `candlestick` | `x` + `y`=**四列** `[开, 收, 低, 高]` | 少给列会明确报错 |
|
|
85
84
|
| `waterfall` | `name` + `value`(+ `total`) | `total` 列非 0 的行当合计项 |
|
|
86
85
|
| `funnel` / `gauge` / `liquid` | `name` + `value` | 仪表盘/水位球只取第一行(多行会警告) |
|
|
87
86
|
| `sankey` | `source` + `target` + `value` | 一张「起点 / 终点 / 流量」的连线表 |
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var n=require("@damoqiongqiu/ice-chart");const e=["line","area","bar","pie","scatter","function","radar","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;
|
|
1
|
+
"use strict";var n=require("@damoqiongqiu/ice-chart");const e=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function firstColumn(n){return Array.isArray(n)?n[0]:n}const t=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series"],i=["x","y","series","size","color","name","value","total","source","target"],o=["line","area","bar","scatter"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(o){const r=[],a=[],fail=(n,e,t)=>r.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>a.push({severity:"warning",code:n,message:e,path:t});if(!o||"object"!=typeof o||Array.isArray(o))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(r,a);void 0!==o.schemaVersion&&1!==o.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${o.schemaVersion}(当前是 1)。`,"schemaVersion"),o.kind?e.includes(o.kind)||fail("unsupported-kind",`不支持的 kind「${o.kind}」。可用类型:${e.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${e.join(" / ")}。`,"kind");for(const n of Object.keys(o))t.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const s=o.kind,l=Array.isArray(o.series)&&o.series.length>0;if(l&&o.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===s||"parametric"===s){const e=String(o.expression||"");if(e.trim()){const t="function"===s?"x":"t",i=o.params&&"object"==typeof o.params?o.params:{},l=function sampleExpression(e,t,i,o,r){const a="function"===r?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):a[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):a[1];if(!(l>s))return[];const u=[],c=64;try{for(let o=0;o<=c;o++){const r=s+(l-s)*o/c,a={...i,[t]:r},m=n.evaluateExpression(e,a);u.push("number"==typeof m?m:NaN)}}catch(n){return[]}return u}(e,t,i,o.domain,s);for(const o of n.diagnoseExpression(e,{variable:t,params:i,values:l})){const n={severity:o.severity,code:`expression-${o.code}`,message:o.message,path:"expression"};("error"===o.severity?r:a).push(n)}}else fail("missing-expression",`kind「${s}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(r,a)}if(l)return validateAnnotation(o.annotation,{kind:s,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(r,a);const u=resolveDataset(o);if(void 0===o.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(r,a);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(r,a);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(r,a);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(r,a);u.rows.forEach((n,e)=>{n.length!==u.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${e}]`)});const c=o.encoding&&"object"==typeof o.encoding?o.encoding:{};o.encoding||fail("missing-encoding",`kind「${s}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const n of Object.keys(c))i.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const m=c.x?columnIndex(u,c.x):-1;validateAnnotation(o.annotation,{kind:s,dataset:u,xIndex:m,numericX:m>=0&&numericRatio(u,m)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${u.columns.join(" / ")}。`,e),-1;const i=columnIndex(u,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${u.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(u,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(u,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if("pie"===s||"funnel"===s||"gauge"===s||"liquid"===s){needColumn(c.name,"encoding.name","pie"===s||"funnel"===s);const n=needNumeric(c.value,"encoding.value",!0);if(n>=0&&"pie"===s){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===s||"liquid"===s)&&u.rows.length>1&&warn("single-value-kind",`kind「${s}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(r,a)}if("radar"===s){const n=needColumn(c.x,"encoding.x",!0),e=needNumeric(toArray(c.y)[0],"encoding.y",!0),t=c.series?needColumn(c.series,"encoding.series",!1):-1;if(toArray(c.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),n>=0&&e>=0){const i=new Set(u.rows.map(e=>String(e[n]))),o=new Set(u.rows.map(n=>t>=0?String(n[t]):"—")),r=u.rows.filter(n=>isFinite(Number(n[e]))).length,a=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),r<a&&warn("radar-missing-value",`有 ${a-r} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(r,a)}if("heatmap"===s)return needColumn(c.x,"encoding.x",!0),needColumn(c.y,"encoding.y",!0),needNumeric(c.value||c.color,c.value?"encoding.value":"encoding.color",!0),finish(r,a);if("waterfall"===s)return needColumn(c.name,"encoding.name",!0),needNumeric(c.value,"encoding.value",!0),c.total&&needColumn(c.total,"encoding.total",!1),finish(r,a);if("sankey"===s){needColumn(c.source,"encoding.source",!0),needColumn(c.target,"encoding.target",!0);const n=needNumeric(c.value,"encoding.value",!0);if(n>=0){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(r,a)}if("line"===s||"area"===s||"bar"===s||"scatter"===s){"scatter"===s&&c.size&&needNumeric(c.size,"encoding.size",!1);const n=toArray(c.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(c.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return c.series&&needColumn(c.series,"encoding.series",!1),finish(r,a)}return warn("passthrough-kind",`kind「${s}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(r,a)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const r=o.includes(e.kind);r||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${o.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,o)=>{if(r&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${o} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,o);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${o} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,o)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${o} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,o)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const r="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,r,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,r,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.series;else if("function"===t||"parametric"===t)i.legend={show:!1},i.tooltip={trigger:"item"},i.crosshair={show:!0,axis:"x",showAxisLabel:!0},i.xAxis={type:"value",name:"x"},i.yAxis={name:"y"},i.series=[{id:"curve",type:t,name:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),r=columnIndex(e,i.value),a=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[r])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?a.slice(0,1):a}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.series),a=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;a.includes(e)||a.push(e);const u=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(u)||s.set(u,new Map),s.get(u).set(e,l)}const l=a.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:a.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:a.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.value||t.color),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);e&&t&&null!==l&&(a.includes(e)||a.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:a},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),r=columnIndex(e,t.total),a=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),a=r>=0?n[r]:void 0;return null!=a&&""!==a&&!1!==a&&0!==Number(a)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:a}]}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),r=columnIndex(e,t.value),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);!e||!t||null===l||l<=0||(a.some(n=>n.name===e)||a.push({name:e}),a.some(n=>n.name===t)||a.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:a,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),r=toArray(i.y),a=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let c;if(l>=0){const n=new Map,i=columnIndex(e,r[0]);for(const t of e.rows){const e=a?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const r=toLabel(t[l])||"(空)";n.has(r)||n.set(r,new Map),n.get(r).set(e,toNumber(t[i]))}c=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else c=r.map((n,i)=>{const r=columnIndex(e,n),l=e.rows.map(n=>{const e=a?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[r]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return a?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const m={legend:{show:c.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:c};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},m.yAxis={name:1===r.length?r[0]:void 0},m}(n,e,t)}}(n,e,t))}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}exports.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","function"],exports.CHART_DSL_KINDS=e,exports.CHART_DSL_SCHEMA_VERSION=1,exports.ChartDslCompileError=ChartDslCompileError,exports.chartDslToJsonString=function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))},exports.columnIndex=columnIndex,exports.compileChartDsl=compileChartDsl,exports.formatDiagnostics=function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"},exports.numericRatio=numericRatio,exports.renderChartDsl=function renderChartDsl(e,t,i){const o=validateChartDsl(t),r=compileChartDsl(t);return{chart:n.createChart(e,r,i),option:r,diagnostics:o}},exports.resolveDataset=resolveDataset,exports.validateChartDsl=validateChartDsl;
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{diagnoseExpression as n,evaluateExpression as e,createChart as t}from"@damoqiongqiu/ice-chart";const i=1,o=["line","area","bar","pie","scatter","function","radar","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};
|
|
1
|
+
import{diagnoseExpression as n,evaluateExpression as e,createChart as t}from"@damoqiongqiu/ice-chart";const i=1,o=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"],r=["line","area","bar","scatter","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","function"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function firstColumn(n){return Array.isArray(n)?n[0]:n}const a=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series"],s=["x","y","series","size","color","name","value","total","source","target"],l=["line","area","bar","scatter"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(t){const i=[],r=[],fail=(n,e,t)=>i.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>r.push({severity:"warning",code:n,message:e,path:t});if(!t||"object"!=typeof t||Array.isArray(t))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(i,r);void 0!==t.schemaVersion&&1!==t.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${t.schemaVersion}(当前是 1)。`,"schemaVersion"),t.kind?o.includes(t.kind)||fail("unsupported-kind",`不支持的 kind「${t.kind}」。可用类型:${o.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${o.join(" / ")}。`,"kind");for(const n of Object.keys(t))a.includes(n)||warn("unknown-field",`未知字段「${n}」会被忽略。`,n);const l=t.kind,u=Array.isArray(t.series)&&t.series.length>0;if(u&&t.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===l||"parametric"===l){const o=String(t.expression||"");if(o.trim()){const a="function"===l?"x":"t",s=t.params&&"object"==typeof t.params?t.params:{},u=function sampleExpression(n,t,i,o,r){const a="function"===r?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):a[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):a[1];if(!(l>s))return[];const u=[],c=64;try{for(let o=0;o<=c;o++){const r=s+(l-s)*o/c,a={...i,[t]:r},m=e(n,a);u.push("number"==typeof m?m:NaN)}}catch(n){return[]}return u}(o,a,s,t.domain,l);for(const e of n(o,{variable:a,params:s,values:u})){const n={severity:e.severity,code:`expression-${e.code}`,message:e.message,path:"expression"};("error"===e.severity?i:r).push(n)}}else fail("missing-expression",`kind「${l}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(i,r)}if(u)return validateAnnotation(t.annotation,{kind:l,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(i,r);const c=resolveDataset(t);if(void 0===t.data)return fail("missing-data",`kind「${l}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(i,r);if(!c)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(i,r);if(!c.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(i,r);if(!c.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(i,r);c.rows.forEach((n,e)=>{n.length!==c.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${c.columns.length}(columns:${c.columns.join(" / ")})。`,`data.rows[${e}]`)});const m=t.encoding&&"object"==typeof t.encoding?t.encoding:{};t.encoding||fail("missing-encoding",`kind「${l}」需要 encoding 把列绑到通道上,例如 { "x": "${c.columns[0]}", "y": "${c.columns[1]||c.columns[0]}" }。`,"encoding");for(const n of Object.keys(m))s.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const d=m.x?columnIndex(c,m.x):-1;validateAnnotation(t.annotation,{kind:l,dataset:c,xIndex:d,numericX:d>=0&&numericRatio(c,d)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${c.columns.join(" / ")}。`,e),-1;const i=columnIndex(c,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${c.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(c,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(c,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if("pie"===l||"funnel"===l||"gauge"===l||"liquid"===l){needColumn(m.name,"encoding.name","pie"===l||"funnel"===l);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0&&"pie"===l){const e=c.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===l||"liquid"===l)&&c.rows.length>1&&warn("single-value-kind",`kind「${l}」只画一个值,${c.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(i,r)}if("radar"===l){const n=needColumn(m.x,"encoding.x",!0),e=needNumeric(toArray(m.y)[0],"encoding.y",!0),t=m.series?needColumn(m.series,"encoding.series",!1):-1;if(toArray(m.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),n>=0&&e>=0){const i=new Set(c.rows.map(e=>String(e[n]))),o=new Set(c.rows.map(n=>t>=0?String(n[t]):"—")),r=c.rows.filter(n=>isFinite(Number(n[e]))).length,a=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),r<a&&warn("radar-missing-value",`有 ${a-r} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(i,r)}if("heatmap"===l)return needColumn(m.x,"encoding.x",!0),needColumn(m.y,"encoding.y",!0),needNumeric(m.value||m.color,m.value?"encoding.value":"encoding.color",!0),finish(i,r);if("waterfall"===l)return needColumn(m.name,"encoding.name",!0),needNumeric(m.value,"encoding.value",!0),m.total&&needColumn(m.total,"encoding.total",!1),finish(i,r);if("sankey"===l){needColumn(m.source,"encoding.source",!0),needColumn(m.target,"encoding.target",!0);const n=needNumeric(m.value,"encoding.value",!0);if(n>=0){const e=c.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(i,r)}if("line"===l||"area"===l||"bar"===l||"scatter"===l){"scatter"===l&&m.size&&needNumeric(m.size,"encoding.size",!1);const n=toArray(m.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${c.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(m.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return m.series&&needColumn(m.series,"encoding.series",!1),finish(i,r)}return warn("passthrough-kind",`kind「${l}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(i,r)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const o=l.includes(e.kind);o||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${l.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,r)=>{if(o&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${r} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,r);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${r} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,r)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${r} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,r)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const r="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,r,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,r,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.series;else if("function"===t||"parametric"===t)i.legend={show:!1},i.tooltip={trigger:"item"},i.crosshair={show:!0,axis:"x",showAxisLabel:!0},i.xAxis={type:"value",name:"x"},i.yAxis={name:"y"},i.series=[{id:"curve",type:t,name:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),r=columnIndex(e,i.value),a=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[r])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?a.slice(0,1):a}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.series),a=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;a.includes(e)||a.push(e);const u=r>=0?toLabel(n[r])||"(空)":String(firstColumn(t.y));s.has(u)||s.set(u,new Map),s.get(u).set(e,l)}const l=a.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:a.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:a.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),r=columnIndex(e,t.value||t.color),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);e&&t&&null!==l&&(a.includes(e)||a.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:a},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),r=columnIndex(e,t.total),a=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),a=r>=0?n[r]:void 0;return null!=a&&""!==a&&!1!==a&&0!==Number(a)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:a}]}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),r=columnIndex(e,t.value),a=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[r]);!e||!t||null===l||l<=0||(a.some(n=>n.name===e)||a.push({name:e}),a.some(n=>n.name===t)||a.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:a,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),r=toArray(i.y),a=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let c;if(l>=0){const n=new Map,i=columnIndex(e,r[0]);for(const t of e.rows){const e=a?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const r=toLabel(t[l])||"(空)";n.has(r)||n.set(r,new Map),n.get(r).set(e,toNumber(t[i]))}c=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>a?[n,e.get(n)??null]:e.get(n)??null)}))}else c=r.map((n,i)=>{const r=columnIndex(e,n),l=e.rows.map(n=>{const e=a?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[r]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return a?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const m={legend:{show:c.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:c};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=a?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},m.yAxis={name:1===r.length?r[0]:void 0},m}(n,e,t)}}(n,e,t))}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))}function renderChartDsl(n,e,i){const o=validateChartDsl(e),r=compileChartDsl(e);return{chart:t(n,r,i),option:r,diagnostics:o}}export{r as CHART_DSL_COMPILED_KINDS,o as CHART_DSL_KINDS,i as CHART_DSL_SCHEMA_VERSION,ChartDslCompileError,chartDslToJsonString,columnIndex,compileChartDsl,formatDiagnostics,numericRatio,renderChartDsl,resolveDataset,validateChartDsl};
|
package/dist/index.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@damoqiongqiu/ice-chart")):"function"==typeof define&&define.amd?define(["exports","@damoqiongqiu/ice-chart"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).ICEChartDSL={},n.ICEChart)}(this,function(n,e){"use strict";const t=["line","area","bar","pie","scatter","function","radar","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});
|
|
1
|
+
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@damoqiongqiu/ice-chart")):"function"==typeof define&&define.amd?define(["exports","@damoqiongqiu/ice-chart"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).ICEChartDSL={},n.ICEChart)}(this,function(n,e){"use strict";const t=["line","area","bar","pie","scatter","function","radar","heatmap","sankey","treemap","gauge","boxplot","waterfall","funnel","graph","parametric","liquid"];function resolveDataset(n){const e=n&&n.data;if(!e)return null;if(Array.isArray(e)){const n=e.filter(n=>n&&"object"==typeof n&&!Array.isArray(n));if(!n.length)return{columns:[],rows:[]};const t=[];for(const e of n)for(const n of Object.keys(e))t.includes(n)||t.push(n);return{columns:t,rows:n.map(n=>t.map(e=>n[e]))}}if("object"!=typeof e)return null;const t=Array.isArray(e.columns)?e.columns.map(n=>String(n)):[],i=Array.isArray(e.rows)?e.rows:[];return t.length?{columns:t,rows:i.map(n=>Array.isArray(n)?n:[])}:null}function columnIndex(n,e){return n&&e?n.columns.indexOf(e):-1}function isNumericCell(n){return"number"==typeof n?isFinite(n):"string"==typeof n&&""!==n.trim()&&isFinite(Number(n))}function numericRatio(n,e){let t=0,i=0;for(const o of n.rows){const n=o[e];null!=n&&""!==n&&(t+=1,isNumericCell(n)&&(i+=1))}return t?i/t:0}function toNumber(n){if("number"==typeof n)return isFinite(n)?n:null;if("string"==typeof n&&""!==n.trim()){const e=Number(n);return isFinite(e)?e:null}return null}function toLabel(n){return null==n?"":String(n)}function toArray(n){return null==n?[]:Array.isArray(n)?n.filter(n=>"string"==typeof n&&n):[n]}function firstColumn(n){return Array.isArray(n)?n[0]:n}const i=["schemaVersion","kind","data","encoding","annotation","title","expression","domain","params","options","series"],o=["x","y","series","size","color","name","value","total","source","target"],a=["line","area","bar","scatter"];function hasAnnotationValue(n){return null!=n&&""!==n}function validateChartDsl(n){const a=[],r=[],fail=(n,e,t)=>a.push({severity:"error",code:n,message:e,path:t}),warn=(n,e,t)=>r.push({severity:"warning",code:n,message:e,path:t});if(!n||"object"!=typeof n||Array.isArray(n))return fail("invalid-root","DSL 根节点必须是一个对象。"),finish(a,r);void 0!==n.schemaVersion&&1!==n.schemaVersion&&fail("unsupported-schema-version",`不支持的 schemaVersion:${n.schemaVersion}(当前是 1)。`,"schemaVersion"),n.kind?t.includes(n.kind)||fail("unsupported-kind",`不支持的 kind「${n.kind}」。可用类型:${t.join(" / ")}。`,"kind"):fail("missing-kind",`缺少 kind。可用类型:${t.join(" / ")}。`,"kind");for(const e of Object.keys(n))i.includes(e)||warn("unknown-field",`未知字段「${e}」会被忽略。`,e);const s=n.kind,l=Array.isArray(n.series)&&n.series.length>0;if(l&&n.data&&warn("series-overrides-data","同时给了 series 和 data:按 series 直通,data / encoding 会被忽略。","series"),"function"===s||"parametric"===s){const t=String(n.expression||"");if(t.trim()){const i="function"===s?"x":"t",o=n.params&&"object"==typeof n.params?n.params:{},l=function sampleExpression(n,t,i,o,a){const r="function"===a?[-10,10]:[0,2*Math.PI],s=Array.isArray(o)&&isFinite(Number(o[0]))?Number(o[0]):r[0],l=Array.isArray(o)&&isFinite(Number(o[1]))?Number(o[1]):r[1];if(!(l>s))return[];const u=[],c=64;try{for(let o=0;o<=c;o++){const a=s+(l-s)*o/c,r={...i,[t]:a},m=e.evaluateExpression(n,r);u.push("number"==typeof m?m:NaN)}}catch(n){return[]}return u}(t,i,o,n.domain,s);for(const n of e.diagnoseExpression(t,{variable:i,params:o,values:l})){const e={severity:n.severity,code:`expression-${n.code}`,message:n.message,path:"expression"};("error"===n.severity?a:r).push(e)}}else fail("missing-expression",`kind「${s}」需要 expression,例如 "sin(x)/x"。`,"expression");return finish(a,r)}if(l)return validateAnnotation(n.annotation,{kind:s,dataset:null,xIndex:-1,numericX:!1},fail,warn),finish(a,r);const u=resolveDataset(n);if(void 0===n.data)return fail("missing-data",`kind「${s}」需要 data(列名 + 行)或直接给 series 直通。`,"data"),finish(a,r);if(!u)return fail("invalid-data","data 需要 { columns, rows } 或 [{ 列名: 值 }, ...] 两种写法之一。","data"),finish(a,r);if(!u.columns.length)return fail("missing-columns","data.columns 不能为空。","data.columns"),finish(a,r);if(!u.rows.length)return fail("empty-data","data.rows 是空的,没有可绘制的数据。","data.rows"),finish(a,r);u.rows.forEach((n,e)=>{n.length!==u.columns.length&&fail("row-length-mismatch",`第 ${e+1} 行有 ${n.length} 个值,但列数是 ${u.columns.length}(columns:${u.columns.join(" / ")})。`,`data.rows[${e}]`)});const c=n.encoding&&"object"==typeof n.encoding?n.encoding:{};n.encoding||fail("missing-encoding",`kind「${s}」需要 encoding 把列绑到通道上,例如 { "x": "${u.columns[0]}", "y": "${u.columns[1]||u.columns[0]}" }。`,"encoding");for(const n of Object.keys(c))o.includes(n)||warn("unknown-encoding",`encoding 里的未知通道「${n}」会被忽略。`,`encoding.${n}`);const m=c.x?columnIndex(u,c.x):-1;validateAnnotation(n.annotation,{kind:s,dataset:u,xIndex:m,numericX:m>=0&&numericRatio(u,m)>.9},fail,warn);const needColumn=(n,e,t)=>{if(!n)return t&&fail("missing-encoding-channel",`${e} 是必填的。可用列:${u.columns.join(" / ")}。`,e),-1;const i=columnIndex(u,n);return i<0?(fail("unknown-column",`列「${n}」不存在。可用列:${u.columns.join(" / ")}。`,e),-1):function hasValue(n,e){return n.rows.some(n=>{const t=n[e];return null!=t&&""!==t})}(u,i)?i:(fail("empty-column",`列「${n}」整列都是空值。`,e),-1)},needNumeric=(n,e,t)=>{const i=needColumn(n,e,t);if(i<0)return-1;const o=numericRatio(u,i);return o<.6?(fail("non-numeric-column",`列「${n}」不是数值列(数字占比 ${(100*o).toFixed(0)}%),不能当 ${e.split(".")[1]} 用。`,e),-1):(o<1&&warn("partly-non-numeric",`列「${n}」有 ${Math.round(100*(1-o))}% 的值不是数字,这些点会被当作空缺。`,e),i)};if("pie"===s||"funnel"===s||"gauge"===s||"liquid"===s){needColumn(c.name,"encoding.name","pie"===s||"funnel"===s);const n=needNumeric(c.value,"encoding.value",!0);if(n>=0&&"pie"===s){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<0});e.length&&warn("pie-negative-value",`饼图 / 玫瑰图不适合表达负值,检测到 ${e.length} 行负数。`,"encoding.value")}return n>=0&&("gauge"===s||"liquid"===s)&&u.rows.length>1&&warn("single-value-kind",`kind「${s}」只画一个值,${u.rows.length} 行数据里只有第一行会被用到。`,"data.rows"),finish(a,r)}if("radar"===s){const n=needColumn(c.x,"encoding.x",!0),e=needNumeric(toArray(c.y)[0],"encoding.y",!0),t=c.series?needColumn(c.series,"encoding.series",!1):-1;if(toArray(c.y).length>1&&warn("radar-single-y","雷达图的 y 只取第一个列名;多组数据请用 encoding.series 指定分组列。","encoding.y"),n>=0&&e>=0){const i=new Set(u.rows.map(e=>String(e[n]))),o=new Set(u.rows.map(n=>t>=0?String(n[t]):"—")),a=u.rows.filter(n=>isFinite(Number(n[e]))).length,r=i.size*o.size;i.size<3&&warn("radar-too-few-indicators",`只有 ${i.size} 个指标,雷达图至少要 3 个才成形。`,"encoding.x"),a<r&&warn("radar-missing-value",`有 ${r-a} 个「指标 × 分组」组合没有数据,会按 0 处理。`,"data.rows")}return finish(a,r)}if("heatmap"===s)return needColumn(c.x,"encoding.x",!0),needColumn(c.y,"encoding.y",!0),needNumeric(c.value||c.color,c.value?"encoding.value":"encoding.color",!0),finish(a,r);if("waterfall"===s)return needColumn(c.name,"encoding.name",!0),needNumeric(c.value,"encoding.value",!0),c.total&&needColumn(c.total,"encoding.total",!1),finish(a,r);if("sankey"===s){needColumn(c.source,"encoding.source",!0),needColumn(c.target,"encoding.target",!0);const n=needNumeric(c.value,"encoding.value",!0);if(n>=0){const e=u.rows.filter(e=>{const t=Number(e[n]);return isFinite(t)&&t<=0}).length;e&&warn("sankey-non-positive",`${e} 行流量不是正数,这些连线会被跳过。`,"encoding.value")}return finish(a,r)}if("line"===s||"area"===s||"bar"===s||"scatter"===s){"scatter"===s&&c.size&&needNumeric(c.size,"encoding.size",!1);const n=toArray(c.y);n.length||fail("missing-encoding-channel",`encoding.y 是必填的(一个列名或一组列名)。可用列:${u.columns.join(" / ")}。`,"encoding.y"),n.length>8&&warn("too-many-series",`y 绑定了 ${n.length} 列,图例会非常挤,建议先筛选。`,"encoding.y"),needColumn(c.x,"encoding.x",!0);for(const e of n)needNumeric(e,"encoding.y",!0);return c.series&&needColumn(c.series,"encoding.series",!1),finish(a,r)}return warn("passthrough-kind",`kind「${s}」暂不支持 data/encoding 编译,请用 series 直通。`,"kind"),finish(a,r)}function finish(n,e){return{valid:0===n.length,errors:n,warnings:e}}function validateAnnotation(n,e,t,i){if(null==n)return;if("object"!=typeof n||Array.isArray(n))return void t("invalid-annotation","annotation 必须是一个对象,形如 { lines, points, areas }。","annotation");for(const e of Object.keys(n))"lines"!==e&&"points"!==e&&"areas"!==e&&i("unknown-annotation-field",`annotation 里的未知字段「${e}」会被忽略(只有 lines / points / areas)。`,`annotation.${e}`);const o=a.includes(e.kind);o||i("annotation-non-cartesian",`kind「${e.kind}」没有 x / y 坐标系,annotation 画不出来(标注只对 ${a.join(" / ")} 有意义)。`,"annotation");const listOf=e=>{const i=n[e];return null==i?[]:Array.isArray(i)?i:(t("invalid-annotation-list",`annotation.${e} 必须是数组。`,`annotation.${e}`),[])},checkValue=(n,t,a)=>{if(o&&e.dataset)if("y"!==t){if(e.numericX)"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${a} 落在数值 x 轴上,建议写数字(收到字符串「${n}」)。`,a);else if(e.xIndex>=0&&"string"==typeof n){const t=e.dataset.rows.map(n=>String(n[e.xIndex]));t.length&&!t.includes(n)&&i("annotation-unknown-category",`${a} 的类目名「${n}」不在 x 列里(该列有 ${new Set(t).size} 个类目),这条标注不会画出来。`,a)}}else"string"!=typeof n||""===n.trim()||isFinite(Number(n))||i("annotation-value-type",`${a} 是数值轴的定位值,建议写数字(收到字符串「${n}」)。`,a)};listOf("lines").forEach((n,e)=>{const i=`annotation.lines[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?hasAnnotationValue(n.value)?checkValue(n.value,"x"===n.axis?"x":"y",`${i}.value`):t("missing-annotation-value",`${i} 缺少 value(数值轴写数字、类目轴写类目名或下标)。`,`${i}.value`):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("points").forEach((n,e)=>{const i=`annotation.points[${e}]`;n&&"object"==typeof n&&!Array.isArray(n)?(hasAnnotationValue(n.x)?checkValue(n.x,"x",`${i}.x`):t("missing-annotation-value",`${i} 缺少 x。`,`${i}.x`),hasAnnotationValue(n.y)?checkValue(n.y,"y",`${i}.y`):t("missing-annotation-value",`${i} 缺少 y。`,`${i}.y`)):t("invalid-annotation-item",`${i} 必须是对象。`,i)}),listOf("areas").forEach((n,e)=>{const o=`annotation.areas[${e}]`;if(!n||"object"!=typeof n||Array.isArray(n))return void t("invalid-annotation-item",`${o} 必须是对象。`,o);const a="x"===n.axis?"x":"y";hasAnnotationValue(n.from)?checkValue(n.from,a,`${o}.from`):t("missing-annotation-value",`${o} 缺少 from(区间起点)。`,`${o}.from`),hasAnnotationValue(n.to)?checkValue(n.to,a,`${o}.to`):t("missing-annotation-value",`${o} 缺少 to(区间终点)。`,`${o}.to`),hasAnnotationValue(n.from)&&hasAnnotationValue(n.to)&&String(n.from)===String(n.to)&&i("annotation-empty-area",`${o} 的 from 与 to 相同,区间宽度为 0,画出来看不见。`,o)})}class ChartDslCompileError extends Error{constructor(n){super(`[ice-chart-dsl] DSL 校验不通过:\n${n.map(n=>`- ${n.message}`).join("\n")}`),this.name="ChartDslCompileError",this.diagnostics=n}}function compileChartDsl(n){const e=validateChartDsl(n);if(!e.valid)throw new ChartDslCompileError(e.errors);const t=n.kind,i={title:normalizeTitle(n),theme:"light",animation:{enter:{duration:520,easing:"easeOutCubic"}},series:[]};if(Array.isArray(n.series)&&n.series.length)i.series=n.series;else if("function"===t||"parametric"===t)i.legend={show:!1},i.tooltip={trigger:"item"},i.crosshair={show:!0,axis:"x",showAxisLabel:!0},i.xAxis={type:"value",name:"x"},i.yAxis={name:"y"},i.series=[{id:"curve",type:t,name:n.title&&"object"==typeof n.title?n.title.text:String(n.title||("function"===t?"f(x)":"curve")),expression:n.expression,domain:n.domain,params:n.params,lineWidth:2}];else{const e=resolveDataset(n);Object.assign(i,function compileByKind(n,e,t){switch(t){case"pie":case"funnel":case"gauge":case"liquid":return function compileNameValue(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.name),a=columnIndex(e,i.value),r=e.rows.map(n=>({name:toLabel(n[o]),value:toNumber(n[a])})).filter(n=>""!==n.name&&isFinite(n.value)),s="gauge"===t||"liquid"===t;return{legend:{show:"pie"===t||"funnel"===t},tooltip:{trigger:"item"},series:[{id:t,type:t,name:i.value,data:s?r.slice(0,1):r}]}}(n,e,t);case"radar":return function compileRadar(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.series),r=[],s=new Map;for(const n of e.rows){const e=toLabel(n[i]),l=toNumber(n[o]);if(!e||null===l)continue;r.includes(e)||r.push(e);const u=a>=0?toLabel(n[a])||"(空)":String(firstColumn(t.y));s.has(u)||s.set(u,new Map),s.get(u).set(e,l)}const l=r.map(n=>{let e=0;for(const t of s.values())e=Math.max(e,Math.abs(t.get(n)??0));return function niceCeil(n){if(!isFinite(n)||n<=0)return 1;const e=Math.pow(10,Math.floor(Math.log10(n))),t=n/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}(e)});return{legend:{show:s.size>1},tooltip:{trigger:"item"},radar:{indicators:r.map((n,e)=>({name:n,max:l[e]})),splitNumber:4},series:[...s.entries()].map(([n,e],t)=>({id:`radar-${t+1}`,type:"radar",name:n,data:r.map(n=>e.get(n)??0)}))}}(n,e);case"heatmap":return function compileHeatmap(n,e){const t=n.encoding||{},i=columnIndex(e,t.x),o=columnIndex(e,firstColumn(t.y)),a=columnIndex(e,t.value||t.color),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);e&&t&&null!==l&&(r.includes(e)||r.push(e),s.push([e,t,l]))}return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.x,data:r},yAxis:{type:"category",name:t.y},grid:{x:!1,y:!1},series:[{id:"heatmap",type:"heatmap",name:t.value,data:s}]}}(n,e);case"waterfall":return function compileWaterfall(n,e){const t=n.encoding||{},i=columnIndex(e,t.name),o=columnIndex(e,t.value),a=columnIndex(e,t.total),r=e.rows.map(n=>{const e=toLabel(n[i]),t=toNumber(n[o]),r=a>=0?n[a]:void 0;return null!=r&&""!==r&&!1!==r&&0!==Number(r)?{name:e,value:isFinite(t)?t:0,total:!0}:{name:e,value:t}}).filter(n=>""!==n.name&&(null===n.value||isFinite(n.value)));return{legend:{show:!1},tooltip:{trigger:"item"},xAxis:{type:"category",name:t.name},yAxis:{name:t.value},series:[{id:"waterfall",type:"waterfall",name:t.value,barWidth:.55,data:r}]}}(n,e);case"sankey":return function compileSankey(n,e){const t=n.encoding||{},i=columnIndex(e,t.source),o=columnIndex(e,t.target),a=columnIndex(e,t.value),r=[],s=[];for(const n of e.rows){const e=toLabel(n[i]),t=toLabel(n[o]),l=toNumber(n[a]);!e||!t||null===l||l<=0||(r.some(n=>n.name===e)||r.push({name:e}),r.some(n=>n.name===t)||r.push({name:t}),s.push({source:e,target:t,value:l}))}return{legend:{show:!1},tooltip:{trigger:"item"},sankey:{nodes:r,links:s,nodeWidth:12,nodePadding:8,linkOpacity:.5},series:[{id:"sankey",type:"sankey",name:"流向"}]}}(n,e);default:return function compileCartesian(n,e,t){const i=n.encoding||{},o=columnIndex(e,i.x),a=toArray(i.y),r=o>=0&&numericRatio(e,o)>.9,s=i.size?columnIndex(e,i.size):-1,l=i.series?columnIndex(e,i.series):-1,u=[],pushX=n=>{u.some(e=>e===n)||u.push(n)};let c;if(l>=0){const n=new Map,i=columnIndex(e,a[0]);for(const t of e.rows){const e=r?toNumber(t[o]):toLabel(t[o]);if(""===e||null===e)continue;pushX(e);const a=toLabel(t[l])||"(空)";n.has(a)||n.set(a,new Map),n.get(a).set(e,toNumber(t[i]))}c=[...n.entries()].map(([n,e],i)=>({id:`series-${i+1}`,type:t,name:n,data:u.map(n=>r?[n,e.get(n)??null]:e.get(n)??null)}))}else c=a.map((n,i)=>{const a=columnIndex(e,n),l=e.rows.map(n=>{const e=r?toNumber(n[o]):toLabel(n[o]),t=toNumber(n[a]);if(""===e||null===e||null===t)return null;if(pushX(e),s>=0){const i=toNumber(n[s]);return[e,t,null===i?8:i]}return r?[e,t]:t}).filter(n=>null!==n);return{id:`series-${i+1}`,type:t,name:n,data:l}});const m={legend:{show:c.length>1},tooltip:{trigger:"scatter"===t?"item":"axis"},series:c};m.crosshair="scatter"===t?{show:!0,axis:"xy",showAxisLabel:!0}:{show:!0,axis:"x",showAxisLabel:!0};return m.xAxis=r?{type:"value",name:i.x}:{type:"category",name:i.x,data:u},m.yAxis={name:1===a.length?a[0]:void 0},m}(n,e,t)}}(n,e,t))}if(n.annotation&&"object"==typeof n.annotation&&(i.annotation=n.annotation),n.options&&"object"==typeof n.options)for(const e of Object.keys(n.options)){const t=n.options[e];void 0!==t&&("series"!==e&&(i[e]=t))}return i}function normalizeTitle(n){if(n.title)return"string"==typeof n.title?{text:n.title}:n.title}n.CHART_DSL_COMPILED_KINDS=["line","area","bar","scatter","waterfall","heatmap","sankey","pie","radar","funnel","gauge","liquid","function"],n.CHART_DSL_KINDS=t,n.CHART_DSL_SCHEMA_VERSION=1,n.ChartDslCompileError=ChartDslCompileError,n.chartDslToJsonString=function chartDslToJsonString(n){return JSON.stringify(compileChartDsl(n))},n.columnIndex=columnIndex,n.compileChartDsl=compileChartDsl,n.formatDiagnostics=function formatDiagnostics(n){const e=[...n.errors,...n.warnings].map(n=>{const e="error"===n.severity?"错误":"警告",t=n.path?`(${n.path})`:"";return`[${e}] ${n.message}${t}`});return e.length?e.join("\n"):"✓ DSL 校验通过"},n.numericRatio=numericRatio,n.renderChartDsl=function renderChartDsl(n,t,i){const o=validateChartDsl(t),a=compileChartDsl(t);return{chart:e.createChart(n,a,i),option:a,diagnostics:o}},n.resolveDataset=resolveDataset,n.validateChartDsl=validateChartDsl});
|
package/dist/types/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export declare const CHART_DSL_SCHEMA_VERSION = 1;
|
|
12
12
|
/** 支持的图表类型。见 `CHART_DSL_COMPILED_KINDS`:没进编译清单的走直通(直接给 `series`)。 */
|
|
13
|
-
export declare const CHART_DSL_KINDS: readonly ["line", "area", "bar", "pie", "scatter", "function", "radar", "
|
|
13
|
+
export declare const CHART_DSL_KINDS: readonly ["line", "area", "bar", "pie", "scatter", "function", "radar", "heatmap", "sankey", "treemap", "gauge", "boxplot", "waterfall", "funnel", "graph", "parametric", "liquid"];
|
|
14
14
|
export type ChartDslKind = (typeof CHART_DSL_KINDS)[number];
|
|
15
15
|
/** 有 data/encoding → option 编译路径的类型(其余直通)。 */
|
|
16
16
|
export declare const CHART_DSL_COMPILED_KINDS: ChartDslKind[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@damoqiongqiu/ice-chart-dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
"verify:full": "npm run verify && npm run test:e2e"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@damoqiongqiu/ice-chart": "^0.
|
|
50
|
-
"ice-render": "^4.
|
|
49
|
+
"@damoqiongqiu/ice-chart": "^0.28.0",
|
|
50
|
+
"ice-render": "^4.2.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@babel/core": "7.28.5",
|
|
54
54
|
"@babel/preset-env": "7.28.5",
|
|
55
55
|
"@babel/preset-typescript": "7.28.5",
|
|
56
|
-
"@damoqiongqiu/ice-chart": "^0.
|
|
56
|
+
"@damoqiongqiu/ice-chart": "^0.28.0",
|
|
57
57
|
"@playwright/test": "^1.63.0",
|
|
58
58
|
"@rollup/plugin-babel": "6.1.0",
|
|
59
59
|
"@rollup/plugin-commonjs": "28.0.1",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@types/jest": "29.5.14",
|
|
64
64
|
"babel-jest": "29.7.0",
|
|
65
65
|
"http-server": "^14.1.1",
|
|
66
|
-
"ice-render": "^4.
|
|
66
|
+
"ice-render": "^4.2.0",
|
|
67
67
|
"jest": "29.7.0",
|
|
68
68
|
"rimraf": "5.0.10",
|
|
69
69
|
"rollup": "3.29.5",
|
|
@@ -26,7 +26,7 @@ This SKILL is the right choice for:
|
|
|
26
26
|
|
|
27
27
|
- business charts: line / area / bar / pie / scatter bubble
|
|
28
28
|
- tables with a second category dimension: radar (indicators), heatmap (row × column matrix)
|
|
29
|
-
-
|
|
29
|
+
- staged totals: waterfall (with a total row)
|
|
30
30
|
- flow tables: sankey (source / target / value)
|
|
31
31
|
- single-value cards: funnel / gauge / liquid
|
|
32
32
|
- math curves: `kind: "function"` (e.g. `sin(x)/x`, damped oscillation)
|
|
@@ -69,7 +69,6 @@ npm install @damoqiongqiu/ice-chart-dsl @damoqiongqiu/ice-chart ice-render
|
|
|
69
69
|
`waterfall` uses them plus an optional `encoding.total` column (non-zero marks the total row).
|
|
70
70
|
- `encoding.x` + `encoding.y` + `encoding.series` make a `radar` (x holds the indicator names).
|
|
71
71
|
- `encoding.x` + `encoding.y` (two category columns) + `encoding.value` make a `heatmap`.
|
|
72
|
-
- `encoding.x` + `encoding.y` **as four columns `[open, close, low, high]`** make a `candlestick`.
|
|
73
72
|
- `encoding.source` + `encoding.target` + `encoding.value` make a `sankey`.
|
|
74
73
|
- `kind: "function"` needs `expression` (+ optional `domain`, `params`) and no data.
|
|
75
74
|
|