@opendata-ai/openchart-engine 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/index.d.ts +366 -0
  2. package/dist/index.js +4227 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +62 -0
  5. package/src/__test-fixtures__/specs.ts +124 -0
  6. package/src/__tests__/axes.test.ts +114 -0
  7. package/src/__tests__/compile-chart.test.ts +337 -0
  8. package/src/__tests__/dimensions.test.ts +151 -0
  9. package/src/__tests__/legend.test.ts +113 -0
  10. package/src/__tests__/scales.test.ts +109 -0
  11. package/src/annotations/__tests__/compute.test.ts +454 -0
  12. package/src/annotations/compute.ts +603 -0
  13. package/src/charts/__tests__/registry.test.ts +110 -0
  14. package/src/charts/bar/__tests__/compute.test.ts +294 -0
  15. package/src/charts/bar/__tests__/labels.test.ts +75 -0
  16. package/src/charts/bar/compute.ts +205 -0
  17. package/src/charts/bar/index.ts +33 -0
  18. package/src/charts/bar/labels.ts +132 -0
  19. package/src/charts/column/__tests__/compute.test.ts +277 -0
  20. package/src/charts/column/compute.ts +282 -0
  21. package/src/charts/column/index.ts +33 -0
  22. package/src/charts/column/labels.ts +108 -0
  23. package/src/charts/dot/__tests__/compute.test.ts +344 -0
  24. package/src/charts/dot/compute.ts +257 -0
  25. package/src/charts/dot/index.ts +46 -0
  26. package/src/charts/dot/labels.ts +97 -0
  27. package/src/charts/line/__tests__/compute.test.ts +437 -0
  28. package/src/charts/line/__tests__/labels.test.ts +93 -0
  29. package/src/charts/line/area.ts +288 -0
  30. package/src/charts/line/compute.ts +177 -0
  31. package/src/charts/line/index.ts +68 -0
  32. package/src/charts/line/labels.ts +144 -0
  33. package/src/charts/pie/__tests__/compute.test.ts +276 -0
  34. package/src/charts/pie/compute.ts +234 -0
  35. package/src/charts/pie/index.ts +49 -0
  36. package/src/charts/pie/labels.ts +142 -0
  37. package/src/charts/registry.ts +64 -0
  38. package/src/charts/scatter/__tests__/compute.test.ts +304 -0
  39. package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
  40. package/src/charts/scatter/compute.ts +124 -0
  41. package/src/charts/scatter/index.ts +41 -0
  42. package/src/charts/scatter/trendline.ts +100 -0
  43. package/src/charts/utils.ts +120 -0
  44. package/src/compile.ts +368 -0
  45. package/src/compiler/__tests__/compile.test.ts +87 -0
  46. package/src/compiler/__tests__/normalize.test.ts +210 -0
  47. package/src/compiler/__tests__/validate.test.ts +440 -0
  48. package/src/compiler/index.ts +47 -0
  49. package/src/compiler/normalize.ts +269 -0
  50. package/src/compiler/types.ts +148 -0
  51. package/src/compiler/validate.ts +581 -0
  52. package/src/graphs/__tests__/community.test.ts +228 -0
  53. package/src/graphs/__tests__/compile-graph.test.ts +315 -0
  54. package/src/graphs/__tests__/encoding.test.ts +314 -0
  55. package/src/graphs/community.ts +92 -0
  56. package/src/graphs/compile-graph.ts +291 -0
  57. package/src/graphs/encoding.ts +302 -0
  58. package/src/graphs/types.ts +98 -0
  59. package/src/index.ts +74 -0
  60. package/src/layout/axes.ts +194 -0
  61. package/src/layout/dimensions.ts +199 -0
  62. package/src/layout/gridlines.ts +84 -0
  63. package/src/layout/scales.ts +426 -0
  64. package/src/legend/compute.ts +186 -0
  65. package/src/tables/__tests__/bar-column.test.ts +147 -0
  66. package/src/tables/__tests__/category-colors.test.ts +153 -0
  67. package/src/tables/__tests__/compile-table.test.ts +208 -0
  68. package/src/tables/__tests__/format-cells.test.ts +126 -0
  69. package/src/tables/__tests__/heatmap.test.ts +124 -0
  70. package/src/tables/__tests__/pagination.test.ts +78 -0
  71. package/src/tables/__tests__/search.test.ts +94 -0
  72. package/src/tables/__tests__/sort.test.ts +107 -0
  73. package/src/tables/__tests__/sparkline.test.ts +122 -0
  74. package/src/tables/bar-column.ts +94 -0
  75. package/src/tables/category-colors.ts +67 -0
  76. package/src/tables/compile-table.ts +420 -0
  77. package/src/tables/format-cells.ts +110 -0
  78. package/src/tables/heatmap.ts +121 -0
  79. package/src/tables/pagination.ts +46 -0
  80. package/src/tables/search.ts +66 -0
  81. package/src/tables/sort.ts +69 -0
  82. package/src/tables/sparkline.ts +113 -0
  83. package/src/tables/utils.ts +16 -0
  84. package/src/tooltips/__tests__/compute.test.ts +328 -0
  85. package/src/tooltips/compute.ts +231 -0
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { compile } from '../index';
3
+ import type { NormalizedChartSpec } from '../types';
4
+
5
+ describe('compile (validate + normalize pipeline)', () => {
6
+ const validSpec = {
7
+ type: 'line',
8
+ data: [
9
+ { date: '2020-01-01', value: 10 },
10
+ { date: '2021-01-01', value: 20 },
11
+ ],
12
+ encoding: {
13
+ x: { field: 'date', type: 'temporal' },
14
+ y: { field: 'value', type: 'quantitative' },
15
+ },
16
+ chrome: { title: 'Test Chart' },
17
+ };
18
+
19
+ it('returns a normalized spec for valid input', () => {
20
+ const result = compile(validSpec);
21
+ expect(result.spec).toBeDefined();
22
+ expect(result.spec.type).toBe('line');
23
+ expect(result.warnings).toBeInstanceOf(Array);
24
+ });
25
+
26
+ it('fills in defaults on the normalized spec', () => {
27
+ const result = compile(validSpec);
28
+ const spec = result.spec as NormalizedChartSpec;
29
+ expect(spec.responsive).toBe(true);
30
+ expect(spec.darkMode).toBe('off');
31
+ expect(spec.annotations).toEqual([]);
32
+ });
33
+
34
+ it('normalizes chrome strings', () => {
35
+ const result = compile(validSpec);
36
+ const spec = result.spec as NormalizedChartSpec;
37
+ expect(spec.chrome.title).toEqual({ text: 'Test Chart' });
38
+ });
39
+
40
+ it('throws on invalid spec', () => {
41
+ expect(() => compile(null)).toThrow('Invalid spec');
42
+ expect(() => compile({})).toThrow('Invalid spec');
43
+ expect(() =>
44
+ compile({
45
+ type: 'line',
46
+ data: [],
47
+ encoding: {},
48
+ }),
49
+ ).toThrow('Invalid spec');
50
+ });
51
+
52
+ it('produces warnings for inferred types', () => {
53
+ const spec = {
54
+ type: 'scatter',
55
+ data: [
56
+ { x: 10, y: 20 },
57
+ { x: 30, y: 40 },
58
+ ],
59
+ encoding: {
60
+ x: { field: 'x' },
61
+ y: { field: 'y' },
62
+ },
63
+ };
64
+
65
+ const result = compile(spec);
66
+ expect(result.warnings.length).toBeGreaterThan(0);
67
+ expect(result.warnings.some((w) => w.includes('Inferred'))).toBe(true);
68
+ });
69
+
70
+ it('works for table specs', () => {
71
+ const result = compile({
72
+ type: 'table',
73
+ data: [{ name: 'Alice', age: 30 }],
74
+ columns: [{ key: 'name' }],
75
+ });
76
+ expect(result.spec.type).toBe('table');
77
+ });
78
+
79
+ it('works for graph specs', () => {
80
+ const result = compile({
81
+ type: 'graph',
82
+ nodes: [{ id: 'a' }, { id: 'b' }],
83
+ edges: [{ source: 'a', target: 'b' }],
84
+ });
85
+ expect(result.spec.type).toBe('graph');
86
+ });
87
+ });
@@ -0,0 +1,210 @@
1
+ import type {
2
+ ChartSpec,
3
+ GraphSpec,
4
+ RangeAnnotation,
5
+ RefLineAnnotation,
6
+ TableSpec,
7
+ TextAnnotation,
8
+ } from '@opendata-ai/openchart-core';
9
+ import { describe, expect, it } from 'vitest';
10
+ import { normalizeSpec } from '../normalize';
11
+ import type { NormalizedChartSpec, NormalizedGraphSpec, NormalizedTableSpec } from '../types';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Test data
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const lineSpec: ChartSpec = {
18
+ type: 'line',
19
+ data: [
20
+ { date: '2020-01-01', value: 10, country: 'US' },
21
+ { date: '2021-01-01', value: 20, country: 'UK' },
22
+ ],
23
+ encoding: {
24
+ x: { field: 'date', type: 'temporal' },
25
+ y: { field: 'value', type: 'quantitative' },
26
+ },
27
+ chrome: {
28
+ title: 'GDP Growth',
29
+ subtitle: { text: 'Over time', style: { fontSize: 16 } },
30
+ },
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Tests
35
+ // ---------------------------------------------------------------------------
36
+
37
+ describe('normalizeSpec', () => {
38
+ describe('chart spec normalization', () => {
39
+ it('fills default values for optionals', () => {
40
+ const warnings: string[] = [];
41
+ const result = normalizeSpec(lineSpec, warnings) as NormalizedChartSpec;
42
+
43
+ expect(result.responsive).toBe(true);
44
+ expect(result.darkMode).toBe('off');
45
+ expect(result.annotations).toEqual([]);
46
+ expect(result.theme).toEqual({});
47
+ expect(result.labels).toEqual({ density: 'auto', format: '', offsets: undefined });
48
+ });
49
+
50
+ it('preserves explicit values', () => {
51
+ const spec: ChartSpec = {
52
+ ...lineSpec,
53
+ responsive: false,
54
+ darkMode: 'force',
55
+ };
56
+ const result = normalizeSpec(spec) as NormalizedChartSpec;
57
+
58
+ expect(result.responsive).toBe(false);
59
+ expect(result.darkMode).toBe('force');
60
+ });
61
+
62
+ it('normalizes chrome strings to ChromeText objects', () => {
63
+ const result = normalizeSpec(lineSpec) as NormalizedChartSpec;
64
+
65
+ // Plain string becomes ChromeText
66
+ expect(result.chrome.title).toEqual({ text: 'GDP Growth' });
67
+ // ChromeText with style is preserved
68
+ expect(result.chrome.subtitle).toEqual({
69
+ text: 'Over time',
70
+ style: { fontSize: 16 },
71
+ });
72
+ // Undefined fields stay undefined
73
+ expect(result.chrome.source).toBeUndefined();
74
+ });
75
+
76
+ it('infers encoding types from data when not specified', () => {
77
+ const warnings: string[] = [];
78
+ const spec: ChartSpec = {
79
+ type: 'scatter',
80
+ data: [
81
+ { x: 10, y: 20 },
82
+ { x: 30, y: 40 },
83
+ ],
84
+ encoding: {
85
+ // No type specified, should be inferred as quantitative
86
+ x: { field: 'x', type: 'quantitative' },
87
+ // biome-ignore lint/suspicious/noExplicitAny: intentionally omitting `type` to test inference
88
+ y: { field: 'y' } as any,
89
+ },
90
+ };
91
+
92
+ const result = normalizeSpec(spec, warnings) as NormalizedChartSpec;
93
+ expect(result.encoding.y?.type).toBe('quantitative');
94
+ expect(warnings.some((w) => w.includes('Inferred'))).toBe(true);
95
+ });
96
+
97
+ it('infers temporal type from date strings', () => {
98
+ const warnings: string[] = [];
99
+ const spec: ChartSpec = {
100
+ type: 'line',
101
+ data: [
102
+ { date: '2020-01-01', value: 10 },
103
+ { date: '2021-06-15', value: 20 },
104
+ ],
105
+ encoding: {
106
+ // biome-ignore lint/suspicious/noExplicitAny: intentionally omitting `type` to test inference
107
+ x: { field: 'date' } as any,
108
+ y: { field: 'value', type: 'quantitative' },
109
+ },
110
+ };
111
+
112
+ const result = normalizeSpec(spec, warnings) as NormalizedChartSpec;
113
+ expect(result.encoding.x?.type).toBe('temporal');
114
+ });
115
+
116
+ it('warns on type mismatch (temporal declared as nominal)', () => {
117
+ const warnings: string[] = [];
118
+ const spec: ChartSpec = {
119
+ type: 'line',
120
+ data: [
121
+ { date: '2020-01-01', value: 10 },
122
+ { date: '2021-06-15', value: 20 },
123
+ ],
124
+ encoding: {
125
+ x: { field: 'date', type: 'nominal' },
126
+ y: { field: 'value', type: 'quantitative' },
127
+ },
128
+ };
129
+
130
+ normalizeSpec(spec, warnings);
131
+ expect(warnings.some((w) => w.includes('looks temporal but was declared as nominal'))).toBe(
132
+ true,
133
+ );
134
+ });
135
+
136
+ it('preserves explicit label config', () => {
137
+ const spec: ChartSpec = {
138
+ ...lineSpec,
139
+ labels: { density: 'none', format: ',.0f' },
140
+ };
141
+ const result = normalizeSpec(spec) as NormalizedChartSpec;
142
+ expect(result.labels).toEqual({ density: 'none', format: ',.0f', offsets: undefined });
143
+ });
144
+
145
+ it('fills partial label config with defaults', () => {
146
+ const spec: ChartSpec = {
147
+ ...lineSpec,
148
+ labels: { density: 'endpoints' },
149
+ };
150
+ const result = normalizeSpec(spec) as NormalizedChartSpec;
151
+ expect(result.labels).toEqual({ density: 'endpoints', format: '', offsets: undefined });
152
+ });
153
+
154
+ it('normalizes annotations with default styles', () => {
155
+ const spec: ChartSpec = {
156
+ ...lineSpec,
157
+ annotations: [
158
+ { type: 'refline', y: 15 },
159
+ { type: 'text', x: '2020-01-01', y: 10, text: 'Start' },
160
+ { type: 'range', y1: 10, y2: 20 },
161
+ ],
162
+ };
163
+
164
+ const result = normalizeSpec(spec) as NormalizedChartSpec;
165
+ const refline = result.annotations[0] as RefLineAnnotation;
166
+ expect(refline.style).toBe('dashed');
167
+ expect(refline.strokeWidth).toBe(1);
168
+
169
+ const text = result.annotations[1] as TextAnnotation;
170
+ expect(text.fontSize).toBe(12);
171
+
172
+ const range = result.annotations[2] as RangeAnnotation;
173
+ expect(range.opacity).toBe(0.1);
174
+ });
175
+ });
176
+
177
+ describe('table spec normalization', () => {
178
+ it('fills default values', () => {
179
+ const spec: TableSpec = {
180
+ type: 'table',
181
+ data: [{ name: 'Alice', age: 30 }],
182
+ columns: [{ key: 'name' }, { key: 'age' }],
183
+ };
184
+
185
+ const result = normalizeSpec(spec) as NormalizedTableSpec;
186
+ expect(result.search).toBe(false);
187
+ expect(result.pagination).toBe(false);
188
+ expect(result.stickyFirstColumn).toBe(false);
189
+ expect(result.compact).toBe(false);
190
+ expect(result.responsive).toBe(true);
191
+ expect(result.darkMode).toBe('off');
192
+ });
193
+ });
194
+
195
+ describe('graph spec normalization', () => {
196
+ it('fills default values', () => {
197
+ const spec: GraphSpec = {
198
+ type: 'graph',
199
+ nodes: [{ id: 'a' }, { id: 'b' }],
200
+ edges: [{ source: 'a', target: 'b' }],
201
+ };
202
+
203
+ const result = normalizeSpec(spec) as NormalizedGraphSpec;
204
+ expect(result.encoding).toEqual({});
205
+ expect(result.layout).toEqual({ type: 'force', chargeStrength: -300, linkDistance: 30 });
206
+ expect(result.annotations).toEqual([]);
207
+ expect(result.darkMode).toBe('off');
208
+ });
209
+ });
210
+ });