@plainviz/core 0.1.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.
@@ -0,0 +1,3 @@
1
+ export { parse } from './parse';
2
+ export type { PlainVizIR, ParseResult, ParseError, ChartType, } from './ir';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EACV,UAAU,EACV,WAAW,EACX,UAAU,EACV,SAAS,GACV,MAAM,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { parse } from './parse';
package/dist/ir.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * PlainViz Intermediate Representation (IR)
3
+ * This is the canonical output of the parser.
4
+ * All renderers consume this format.
5
+ */
6
+ export type ChartType = 'bar' | 'line' | 'pie' | 'area';
7
+ export interface PlainVizIR {
8
+ type: ChartType;
9
+ title?: string;
10
+ subtitle?: string;
11
+ labels: string[];
12
+ values: number[];
13
+ meta?: {
14
+ xAxis?: string;
15
+ yAxis?: string;
16
+ theme?: string;
17
+ };
18
+ }
19
+ export interface ParseError {
20
+ line: number;
21
+ message: string;
22
+ }
23
+ export type ParseResult = {
24
+ ok: true;
25
+ ir: PlainVizIR;
26
+ } | {
27
+ ok: false;
28
+ errors: ParseError[];
29
+ };
30
+ //# sourceMappingURL=ir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../src/ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,UAAU,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC"}
package/dist/ir.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * PlainViz Intermediate Representation (IR)
3
+ * This is the canonical output of the parser.
4
+ * All renderers consume this format.
5
+ */
6
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * PlainViz Parser
3
+ * Parses PlainViz syntax into IR (Intermediate Representation)
4
+ */
5
+ import type { ParseResult } from './ir';
6
+ export declare function parse(input: string): ParseResult;
7
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAc,WAAW,EAAyB,MAAM,MAAM,CAAC;AAiB3E,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAuHhD"}
package/dist/parse.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * PlainViz Parser
3
+ * Parses PlainViz syntax into IR (Intermediate Representation)
4
+ */
5
+ const HEADER_KEYS = new Set([
6
+ 'type', 'title', 'subtitle', 'theme',
7
+ 'x-axis', 'y-axis', 'x', 'y'
8
+ ]);
9
+ function isHeaderKey(key) {
10
+ return HEADER_KEYS.has(key.toLowerCase());
11
+ }
12
+ function cleanNumber(value) {
13
+ // Remove common formatting: $, %, commas, spaces
14
+ const cleaned = value.replace(/[$%,\s]/g, '');
15
+ return parseFloat(cleaned);
16
+ }
17
+ export function parse(input) {
18
+ const lines = input.split('\n');
19
+ const errors = [];
20
+ let type = 'bar';
21
+ let title;
22
+ let subtitle;
23
+ let xAxis;
24
+ let yAxis;
25
+ let theme;
26
+ const labels = [];
27
+ const values = [];
28
+ let inDataSection = false;
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const lineNum = i + 1;
31
+ const line = lines[i];
32
+ const trimmed = line.trim();
33
+ // Skip empty lines and comments
34
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) {
35
+ continue;
36
+ }
37
+ // Find colon separator
38
+ const colonIndex = trimmed.indexOf(':');
39
+ if (colonIndex === -1) {
40
+ errors.push({ line: lineNum, message: `Missing ':' separator` });
41
+ continue;
42
+ }
43
+ const key = trimmed.slice(0, colonIndex).trim();
44
+ const value = trimmed.slice(colonIndex + 1).trim();
45
+ if (!key) {
46
+ errors.push({ line: lineNum, message: `Empty key before ':'` });
47
+ continue;
48
+ }
49
+ // Parse header fields
50
+ if (!inDataSection && isHeaderKey(key)) {
51
+ const keyLower = key.toLowerCase();
52
+ switch (keyLower) {
53
+ case 'type':
54
+ const validTypes = ['bar', 'line', 'pie', 'area'];
55
+ const typeLower = value.toLowerCase();
56
+ if (validTypes.includes(typeLower)) {
57
+ type = typeLower;
58
+ }
59
+ else {
60
+ errors.push({
61
+ line: lineNum,
62
+ message: `Invalid chart type '${value}'. Valid types: ${validTypes.join(', ')}`
63
+ });
64
+ }
65
+ break;
66
+ case 'title':
67
+ title = value.replace(/^["']|["']$/g, '');
68
+ break;
69
+ case 'subtitle':
70
+ subtitle = value.replace(/^["']|["']$/g, '');
71
+ break;
72
+ case 'x-axis':
73
+ case 'x':
74
+ xAxis = value;
75
+ break;
76
+ case 'y-axis':
77
+ case 'y':
78
+ yAxis = value;
79
+ break;
80
+ case 'theme':
81
+ theme = value;
82
+ break;
83
+ }
84
+ }
85
+ else {
86
+ // Data section
87
+ inDataSection = true;
88
+ const numValue = cleanNumber(value);
89
+ if (isNaN(numValue)) {
90
+ errors.push({
91
+ line: lineNum,
92
+ message: `Invalid number '${value}' for label '${key}'`
93
+ });
94
+ continue;
95
+ }
96
+ labels.push(key);
97
+ values.push(numValue);
98
+ }
99
+ }
100
+ // Validation
101
+ if (labels.length === 0 && errors.length === 0) {
102
+ errors.push({ line: 0, message: 'No data points found' });
103
+ }
104
+ if (errors.length > 0) {
105
+ return { ok: false, errors };
106
+ }
107
+ const ir = {
108
+ type,
109
+ labels,
110
+ values,
111
+ };
112
+ if (title)
113
+ ir.title = title;
114
+ if (subtitle)
115
+ ir.subtitle = subtitle;
116
+ if (xAxis || yAxis || theme) {
117
+ ir.meta = {};
118
+ if (xAxis)
119
+ ir.meta.xAxis = xAxis;
120
+ if (yAxis)
121
+ ir.meta.yAxis = yAxis;
122
+ if (theme)
123
+ ir.meta.theme = theme;
124
+ }
125
+ return { ok: true, ir };
126
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parse.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../src/parse.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parse } from './parse';
3
+ describe('parse', () => {
4
+ describe('basic parsing', () => {
5
+ it('parses simple bar chart', () => {
6
+ const result = parse(`
7
+ Type: Bar
8
+ Title: Test
9
+
10
+ A: 10
11
+ B: 20
12
+ `);
13
+ expect(result.ok).toBe(true);
14
+ if (result.ok) {
15
+ expect(result.ir.type).toBe('bar');
16
+ expect(result.ir.title).toBe('Test');
17
+ expect(result.ir.labels).toEqual(['A', 'B']);
18
+ expect(result.ir.values).toEqual([10, 20]);
19
+ }
20
+ });
21
+ it('defaults to bar chart when type not specified', () => {
22
+ const result = parse(`
23
+ A: 10
24
+ B: 20
25
+ `);
26
+ expect(result.ok).toBe(true);
27
+ if (result.ok) {
28
+ expect(result.ir.type).toBe('bar');
29
+ }
30
+ });
31
+ });
32
+ describe('number cleaning', () => {
33
+ it('handles currency format ($1,200)', () => {
34
+ const result = parse(`
35
+ Price: $1,200
36
+ `);
37
+ expect(result.ok).toBe(true);
38
+ if (result.ok) {
39
+ expect(result.ir.values).toEqual([1200]);
40
+ }
41
+ });
42
+ it('handles percentage format (50%)', () => {
43
+ const result = parse(`
44
+ Rate: 50%
45
+ `);
46
+ expect(result.ok).toBe(true);
47
+ if (result.ok) {
48
+ expect(result.ir.values).toEqual([50]);
49
+ }
50
+ });
51
+ it('handles numbers with commas (1,000,000)', () => {
52
+ const result = parse(`
53
+ Big: 1,000,000
54
+ `);
55
+ expect(result.ok).toBe(true);
56
+ if (result.ok) {
57
+ expect(result.ir.values).toEqual([1000000]);
58
+ }
59
+ });
60
+ });
61
+ describe('whitespace handling', () => {
62
+ it('ignores empty lines', () => {
63
+ const result = parse(`
64
+ Type: Bar
65
+
66
+ A: 10
67
+
68
+ B: 20
69
+
70
+ `);
71
+ expect(result.ok).toBe(true);
72
+ if (result.ok) {
73
+ expect(result.ir.labels).toEqual(['A', 'B']);
74
+ }
75
+ });
76
+ it('ignores comment lines (// and #)', () => {
77
+ const result = parse(`
78
+ // This is a comment
79
+ Type: Bar
80
+ # Another comment
81
+ A: 10
82
+ `);
83
+ expect(result.ok).toBe(true);
84
+ if (result.ok) {
85
+ expect(result.ir.labels).toEqual(['A']);
86
+ }
87
+ });
88
+ });
89
+ describe('error handling', () => {
90
+ it('reports error for missing colon', () => {
91
+ const result = parse(`
92
+ Type: Bar
93
+ A 10
94
+ `);
95
+ expect(result.ok).toBe(false);
96
+ if (!result.ok) {
97
+ expect(result.errors[0].message).toContain("Missing ':'");
98
+ expect(result.errors[0].line).toBe(3);
99
+ }
100
+ });
101
+ it('reports error for invalid number', () => {
102
+ const result = parse(`
103
+ Type: Bar
104
+ A: abc
105
+ `);
106
+ expect(result.ok).toBe(false);
107
+ if (!result.ok) {
108
+ expect(result.errors[0].message).toContain("Invalid number");
109
+ }
110
+ });
111
+ it('reports error for invalid chart type', () => {
112
+ const result = parse(`
113
+ Type: InvalidType
114
+ A: 10
115
+ `);
116
+ expect(result.ok).toBe(false);
117
+ if (!result.ok) {
118
+ expect(result.errors[0].message).toContain("Invalid chart type");
119
+ }
120
+ });
121
+ it('reports error when no data points', () => {
122
+ const result = parse(`
123
+ Type: Bar
124
+ Title: Empty
125
+ `);
126
+ expect(result.ok).toBe(false);
127
+ if (!result.ok) {
128
+ expect(result.errors[0].message).toContain("No data points");
129
+ }
130
+ });
131
+ });
132
+ describe('title parsing', () => {
133
+ it('strips quotes from title', () => {
134
+ const result = parse(`
135
+ Title: "Quoted Title"
136
+ A: 10
137
+ `);
138
+ expect(result.ok).toBe(true);
139
+ if (result.ok) {
140
+ expect(result.ir.title).toBe('Quoted Title');
141
+ }
142
+ });
143
+ it('handles title without quotes', () => {
144
+ const result = parse(`
145
+ Title: Unquoted Title
146
+ A: 10
147
+ `);
148
+ expect(result.ok).toBe(true);
149
+ if (result.ok) {
150
+ expect(result.ir.title).toBe('Unquoted Title');
151
+ }
152
+ });
153
+ });
154
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@plainviz/core",
3
+ "version": "0.1.0",
4
+ "description": "PlainViz core parser and IR",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsc --watch",
21
+ "test": "vitest"
22
+ },
23
+ "keywords": [
24
+ "plainviz",
25
+ "parser",
26
+ "chart",
27
+ "visualization"
28
+ ],
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "typescript": "^5.7.2",
32
+ "vitest": "^3.0.0"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { parse } from './parse';
2
+ export type {
3
+ PlainVizIR,
4
+ ParseResult,
5
+ ParseError,
6
+ ChartType,
7
+ } from './ir';
package/src/ir.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PlainViz Intermediate Representation (IR)
3
+ * This is the canonical output of the parser.
4
+ * All renderers consume this format.
5
+ */
6
+
7
+ export type ChartType = 'bar' | 'line' | 'pie' | 'area';
8
+
9
+ export interface PlainVizIR {
10
+ type: ChartType;
11
+ title?: string;
12
+ subtitle?: string;
13
+ labels: string[];
14
+ values: number[];
15
+ meta?: {
16
+ xAxis?: string;
17
+ yAxis?: string;
18
+ theme?: string;
19
+ };
20
+ }
21
+
22
+ export interface ParseError {
23
+ line: number;
24
+ message: string;
25
+ }
26
+
27
+ export type ParseResult =
28
+ | { ok: true; ir: PlainVizIR }
29
+ | { ok: false; errors: ParseError[] };
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parse } from './parse';
3
+
4
+ describe('parse', () => {
5
+ describe('basic parsing', () => {
6
+ it('parses simple bar chart', () => {
7
+ const result = parse(`
8
+ Type: Bar
9
+ Title: Test
10
+
11
+ A: 10
12
+ B: 20
13
+ `);
14
+ expect(result.ok).toBe(true);
15
+ if (result.ok) {
16
+ expect(result.ir.type).toBe('bar');
17
+ expect(result.ir.title).toBe('Test');
18
+ expect(result.ir.labels).toEqual(['A', 'B']);
19
+ expect(result.ir.values).toEqual([10, 20]);
20
+ }
21
+ });
22
+
23
+ it('defaults to bar chart when type not specified', () => {
24
+ const result = parse(`
25
+ A: 10
26
+ B: 20
27
+ `);
28
+ expect(result.ok).toBe(true);
29
+ if (result.ok) {
30
+ expect(result.ir.type).toBe('bar');
31
+ }
32
+ });
33
+ });
34
+
35
+ describe('number cleaning', () => {
36
+ it('handles currency format ($1,200)', () => {
37
+ const result = parse(`
38
+ Price: $1,200
39
+ `);
40
+ expect(result.ok).toBe(true);
41
+ if (result.ok) {
42
+ expect(result.ir.values).toEqual([1200]);
43
+ }
44
+ });
45
+
46
+ it('handles percentage format (50%)', () => {
47
+ const result = parse(`
48
+ Rate: 50%
49
+ `);
50
+ expect(result.ok).toBe(true);
51
+ if (result.ok) {
52
+ expect(result.ir.values).toEqual([50]);
53
+ }
54
+ });
55
+
56
+ it('handles numbers with commas (1,000,000)', () => {
57
+ const result = parse(`
58
+ Big: 1,000,000
59
+ `);
60
+ expect(result.ok).toBe(true);
61
+ if (result.ok) {
62
+ expect(result.ir.values).toEqual([1000000]);
63
+ }
64
+ });
65
+ });
66
+
67
+ describe('whitespace handling', () => {
68
+ it('ignores empty lines', () => {
69
+ const result = parse(`
70
+ Type: Bar
71
+
72
+ A: 10
73
+
74
+ B: 20
75
+
76
+ `);
77
+ expect(result.ok).toBe(true);
78
+ if (result.ok) {
79
+ expect(result.ir.labels).toEqual(['A', 'B']);
80
+ }
81
+ });
82
+
83
+ it('ignores comment lines (// and #)', () => {
84
+ const result = parse(`
85
+ // This is a comment
86
+ Type: Bar
87
+ # Another comment
88
+ A: 10
89
+ `);
90
+ expect(result.ok).toBe(true);
91
+ if (result.ok) {
92
+ expect(result.ir.labels).toEqual(['A']);
93
+ }
94
+ });
95
+ });
96
+
97
+ describe('error handling', () => {
98
+ it('reports error for missing colon', () => {
99
+ const result = parse(`
100
+ Type: Bar
101
+ A 10
102
+ `);
103
+ expect(result.ok).toBe(false);
104
+ if (!result.ok) {
105
+ expect(result.errors[0].message).toContain("Missing ':'");
106
+ expect(result.errors[0].line).toBe(3);
107
+ }
108
+ });
109
+
110
+ it('reports error for invalid number', () => {
111
+ const result = parse(`
112
+ Type: Bar
113
+ A: abc
114
+ `);
115
+ expect(result.ok).toBe(false);
116
+ if (!result.ok) {
117
+ expect(result.errors[0].message).toContain("Invalid number");
118
+ }
119
+ });
120
+
121
+ it('reports error for invalid chart type', () => {
122
+ const result = parse(`
123
+ Type: InvalidType
124
+ A: 10
125
+ `);
126
+ expect(result.ok).toBe(false);
127
+ if (!result.ok) {
128
+ expect(result.errors[0].message).toContain("Invalid chart type");
129
+ }
130
+ });
131
+
132
+ it('reports error when no data points', () => {
133
+ const result = parse(`
134
+ Type: Bar
135
+ Title: Empty
136
+ `);
137
+ expect(result.ok).toBe(false);
138
+ if (!result.ok) {
139
+ expect(result.errors[0].message).toContain("No data points");
140
+ }
141
+ });
142
+ });
143
+
144
+ describe('title parsing', () => {
145
+ it('strips quotes from title', () => {
146
+ const result = parse(`
147
+ Title: "Quoted Title"
148
+ A: 10
149
+ `);
150
+ expect(result.ok).toBe(true);
151
+ if (result.ok) {
152
+ expect(result.ir.title).toBe('Quoted Title');
153
+ }
154
+ });
155
+
156
+ it('handles title without quotes', () => {
157
+ const result = parse(`
158
+ Title: Unquoted Title
159
+ A: 10
160
+ `);
161
+ expect(result.ok).toBe(true);
162
+ if (result.ok) {
163
+ expect(result.ir.title).toBe('Unquoted Title');
164
+ }
165
+ });
166
+ });
167
+ });
package/src/parse.ts ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * PlainViz Parser
3
+ * Parses PlainViz syntax into IR (Intermediate Representation)
4
+ */
5
+
6
+ import type { PlainVizIR, ParseResult, ParseError, ChartType } from './ir';
7
+
8
+ const HEADER_KEYS = new Set([
9
+ 'type', 'title', 'subtitle', 'theme',
10
+ 'x-axis', 'y-axis', 'x', 'y'
11
+ ]);
12
+
13
+ function isHeaderKey(key: string): boolean {
14
+ return HEADER_KEYS.has(key.toLowerCase());
15
+ }
16
+
17
+ function cleanNumber(value: string): number {
18
+ // Remove common formatting: $, %, commas, spaces
19
+ const cleaned = value.replace(/[$%,\s]/g, '');
20
+ return parseFloat(cleaned);
21
+ }
22
+
23
+ export function parse(input: string): ParseResult {
24
+ const lines = input.split('\n');
25
+ const errors: ParseError[] = [];
26
+
27
+ let type: ChartType = 'bar';
28
+ let title: string | undefined;
29
+ let subtitle: string | undefined;
30
+ let xAxis: string | undefined;
31
+ let yAxis: string | undefined;
32
+ let theme: string | undefined;
33
+
34
+ const labels: string[] = [];
35
+ const values: number[] = [];
36
+
37
+ let inDataSection = false;
38
+
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const lineNum = i + 1;
41
+ const line = lines[i];
42
+ const trimmed = line.trim();
43
+
44
+ // Skip empty lines and comments
45
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) {
46
+ continue;
47
+ }
48
+
49
+ // Find colon separator
50
+ const colonIndex = trimmed.indexOf(':');
51
+ if (colonIndex === -1) {
52
+ errors.push({ line: lineNum, message: `Missing ':' separator` });
53
+ continue;
54
+ }
55
+
56
+ const key = trimmed.slice(0, colonIndex).trim();
57
+ const value = trimmed.slice(colonIndex + 1).trim();
58
+
59
+ if (!key) {
60
+ errors.push({ line: lineNum, message: `Empty key before ':'` });
61
+ continue;
62
+ }
63
+
64
+ // Parse header fields
65
+ if (!inDataSection && isHeaderKey(key)) {
66
+ const keyLower = key.toLowerCase();
67
+
68
+ switch (keyLower) {
69
+ case 'type':
70
+ const validTypes = ['bar', 'line', 'pie', 'area'];
71
+ const typeLower = value.toLowerCase();
72
+ if (validTypes.includes(typeLower)) {
73
+ type = typeLower as ChartType;
74
+ } else {
75
+ errors.push({
76
+ line: lineNum,
77
+ message: `Invalid chart type '${value}'. Valid types: ${validTypes.join(', ')}`
78
+ });
79
+ }
80
+ break;
81
+ case 'title':
82
+ title = value.replace(/^["']|["']$/g, '');
83
+ break;
84
+ case 'subtitle':
85
+ subtitle = value.replace(/^["']|["']$/g, '');
86
+ break;
87
+ case 'x-axis':
88
+ case 'x':
89
+ xAxis = value;
90
+ break;
91
+ case 'y-axis':
92
+ case 'y':
93
+ yAxis = value;
94
+ break;
95
+ case 'theme':
96
+ theme = value;
97
+ break;
98
+ }
99
+ } else {
100
+ // Data section
101
+ inDataSection = true;
102
+
103
+ const numValue = cleanNumber(value);
104
+ if (isNaN(numValue)) {
105
+ errors.push({
106
+ line: lineNum,
107
+ message: `Invalid number '${value}' for label '${key}'`
108
+ });
109
+ continue;
110
+ }
111
+
112
+ labels.push(key);
113
+ values.push(numValue);
114
+ }
115
+ }
116
+
117
+ // Validation
118
+ if (labels.length === 0 && errors.length === 0) {
119
+ errors.push({ line: 0, message: 'No data points found' });
120
+ }
121
+
122
+ if (errors.length > 0) {
123
+ return { ok: false, errors };
124
+ }
125
+
126
+ const ir: PlainVizIR = {
127
+ type,
128
+ labels,
129
+ values,
130
+ };
131
+
132
+ if (title) ir.title = title;
133
+ if (subtitle) ir.subtitle = subtitle;
134
+ if (xAxis || yAxis || theme) {
135
+ ir.meta = {};
136
+ if (xAxis) ir.meta.xAxis = xAxis;
137
+ if (yAxis) ir.meta.yAxis = yAxis;
138
+ if (theme) ir.meta.theme = theme;
139
+ }
140
+
141
+ return { ok: true, ir };
142
+ }