Setting the file. One moment.
Validate Option · Dreambase ECharts · DreambaseAI/skills · Skills Docs
ContentsBack to the top of the page scripts/validate-option.mjs
scripts/ validate-option.mjs
JavaScript · 127 lines · 6 KB
;
14 import { fileURLToPath } from "node:url" ;
15
16 const INDEX_PATH = resolve ( dirname ( fileURLToPath ( import . meta .url)), "../assets/option-index.json" );
17 const { root } = JSON . parse ( readFileSync ( INDEX_PATH , "utf8" ));
18 const KNOWN_TOP = new Set (root. map (( n ) => n.p). concat ([ "baseOption" , "media" ]));
19 const seriesNode = root. find (( n ) => n.p === "series" );
20 const KNOWN_SERIES_TYPES = new Set (seriesNode.c. map (( n ) => n.p));
21 const SERIES_PROPS = new Map (seriesNode.c. map (( n ) => [n.p, new Set ((n.c ?? []). map (( c ) => c.p))]));
22
23 const MAX_INLINE_POINTS = 200 ;
24 const errors = [];
25 const warnings = [];
26 const arr = ( v ) => (v === undefined ? [] : Array. isArray (v) ? v : [v]);
27
28 function validateOption ( opt , ctx = "" ) {
29 const at = ( p ) => (ctx ? `${ ctx }.${ p }` : p);
30
31 for ( const key of Object. keys (opt)) {
32 if ( ! KNOWN_TOP . has (key)) warnings. push ( `${ at ( key ) }: unknown top-level option (typo? run echarts-option.mjs --find ${ key })` );
33 }
34
35 const xAxes = arr (opt.xAxis);
36 const yAxes = arr (opt.yAxis);
37 const series = arr (opt.series);
38 const hasDataset = opt.dataset !== undefined ;
39
40 const stacks = new Map ();
41 series. forEach (( s , i ) => {
42 const sat = at ( `series[${ i }]` );
43 if ( ! s || typeof s !== "object" ) return errors. push ( `${ sat }: not an object` );
44
45 if ( ! s.type) errors. push ( `${ sat }: missing "type"` );
46 else if ( ! KNOWN_SERIES_TYPES . has (s.type)) errors. push ( `${ sat }: unknown series type '${ s . type }' (valid: ${ [ ... KNOWN_SERIES_TYPES ]. join ( ", " ) })` );
47
48 const known = SERIES_PROPS . get (s.type);
49 if (known) {
50 for ( const k of Object. keys (s)) {
51 if ( ! known. has (k)) warnings. push ( `${ sat }.${ k }: not a documented option for series.${ s . type } (typo? run echarts-option.mjs series-${ s . type })` );
52 }
53 }
54
55 if (s.xAxisIndex !== undefined && s.xAxisIndex >= Math. max (xAxes. length , 1 )) errors. push ( `${ sat }.xAxisIndex=${ s . xAxisIndex } but only ${ xAxes . length } xAxis defined` );
56 if (s.yAxisIndex !== undefined && s.yAxisIndex >= Math. max (yAxes. length , 1 )) errors. push ( `${ sat }.yAxisIndex=${ s . yAxisIndex } but only ${ yAxes . length } yAxis defined` );
57
58 if (s.stack !== undefined ) stacks. set (s.stack, (stacks. get (s.stack) ?? 0 ) + 1 );
59
60 if (Array. isArray (s.data) && s.data. length > MAX_INLINE_POINTS ) {
61 warnings. push ( `${ sat }.data has ${ s . data . length } inline points (>${ MAX_INLINE_POINTS }) — move to dataset.source` );
62 }
63
64 if (s.type === "heatmap" && opt.visualMap === undefined ) errors. push ( `${ sat }: heatmap requires a visualMap component — nothing renders without it` );
65 if (s.type === "radar" && opt.radar?.indicator === undefined ) errors. push ( `${ sat }: radar series requires radar.indicator at the top level` );
66 if (s.type === "candlestick" && Array. isArray (s.data)) {
67 const bad = s.data. findIndex (( d ) => Array. isArray (d) && d. length !== 4 && d. length !== 5 );
68 if (bad !== - 1 ) errors. push ( `${ sat }.data[${ bad }]: candlestick tuples must be [open, close, lowest, highest]` );
69 }
70 if (s.type === "sankey" ) {
71 const nodes = s.data ?? s.nodes;
72 const links = s.links ?? s.edges;
73 if ( ! nodes || ! links) errors. push ( `${ sat }: sankey needs both data/nodes and links/edges` );
74 else {
75 const names = new Set (nodes. map (( n ) => n.name));
76 const badLink = links. findIndex (( l ) => ! names. has (l.source) || ! names. has (l.target));
77 if (badLink !== - 1 ) errors. push ( `${ sat }.links[${ badLink }]: source/target not present in node names` );
78 }
79 }
80 if (s.type === "pie" && (opt.xAxis || opt.yAxis)) warnings. push ( `${ sat }: pie ignores xAxis/yAxis — remove them unless another series needs them` );
81 });
82
83 for ( const [ name , count ] of stacks) {
84 if (count === 1 ) warnings. push ( `${ at ( "series" ) }: stack '${ name }' used by only one series — stacking needs the same stack string on every stacked series` );
85 }
86
87 xAxes. forEach (( ax , i ) => {
88 if (ax?.type === "category" && ! ax.data && ! hasDataset) warnings. push ( `${ at ( `xAxis[${ i }]` ) }: category axis has no data and no dataset — categories come from one or the other` );
89 });
90
91 arr (opt.dataZoom). forEach (( dz , i ) => {
92 if (dz?.type && ! [ "inside" , "slider" ]. includes (dz.type)) errors. push ( `${ at ( `dataZoom[${ i }]` ) }: type must be 'inside' or 'slider'` );
93 });
94
95 const cartesianMulti = series. filter (( s ) => [ "line" , "bar" , "scatter" ]. includes (s?.type)). length > 1 ;
96 if (cartesianMulti) {
97 if (opt.tooltip === undefined ) warnings. push ( `${ at ( "tooltip" ) }: multi-series chart without tooltip — add { "trigger": "axis" } unless the user opted out` );
98 if (opt.legend === undefined ) warnings. push ( `${ at ( "legend" ) }: multi-series chart without legend — users can't tell series apart` );
99 }
100 }
101
102 // --- main ---
103 const src = process.argv[ 2 ]
104 ? readFileSync (process.argv[ 2 ], "utf8" )
105 : readFileSync ( 0 , "utf8" );
106
107 let opt;
108 try {
109 opt = JSON . parse (src);
110 } catch (e) {
111 console. error ( `error: not valid JSON — ${ e . message }` );
112 process. exit ( 1 );
113 }
114
115 if (opt.baseOption) {
116 validateOption (opt.baseOption, "baseOption" );
117 arr (opt.media). forEach (( m , i ) => {
118 if ( ! m.option) warnings. push ( `media[${ i }]: missing "option"` );
119 });
120 } else {
121 validateOption (opt);
122 }
123
124 for ( const e of errors) console. log ( `error: ${ e }` );
125 for ( const w of warnings) console. log ( `warning: ${ w }` );
126 if ( ! errors. length && ! warnings. length ) console. log ( "ok: no issues found" );
127 process. exit (errors. length ? 1 : 0 );