@opendata-ai/openchart-core 2.0.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 (51) hide show
  1. package/README.md +130 -0
  2. package/dist/index.d.ts +2030 -0
  3. package/dist/index.js +1176 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/styles.css +757 -0
  6. package/package.json +61 -0
  7. package/src/accessibility/__tests__/alt-text.test.ts +110 -0
  8. package/src/accessibility/__tests__/aria.test.ts +125 -0
  9. package/src/accessibility/alt-text.ts +120 -0
  10. package/src/accessibility/aria.ts +73 -0
  11. package/src/accessibility/index.ts +6 -0
  12. package/src/colors/__tests__/colorblind.test.ts +63 -0
  13. package/src/colors/__tests__/contrast.test.ts +71 -0
  14. package/src/colors/__tests__/palettes.test.ts +54 -0
  15. package/src/colors/colorblind.ts +122 -0
  16. package/src/colors/contrast.ts +94 -0
  17. package/src/colors/index.ts +27 -0
  18. package/src/colors/palettes.ts +118 -0
  19. package/src/helpers/__tests__/spec-builders.test.ts +336 -0
  20. package/src/helpers/spec-builders.ts +410 -0
  21. package/src/index.ts +129 -0
  22. package/src/labels/__tests__/collision.test.ts +197 -0
  23. package/src/labels/collision.ts +154 -0
  24. package/src/labels/index.ts +6 -0
  25. package/src/layout/__tests__/chrome.test.ts +114 -0
  26. package/src/layout/__tests__/text-measure.test.ts +49 -0
  27. package/src/layout/chrome.ts +223 -0
  28. package/src/layout/index.ts +6 -0
  29. package/src/layout/text-measure.ts +54 -0
  30. package/src/locale/__tests__/format.test.ts +90 -0
  31. package/src/locale/format.ts +132 -0
  32. package/src/locale/index.ts +6 -0
  33. package/src/responsive/__tests__/breakpoints.test.ts +58 -0
  34. package/src/responsive/breakpoints.ts +92 -0
  35. package/src/responsive/index.ts +18 -0
  36. package/src/styles/viz.css +757 -0
  37. package/src/theme/__tests__/dark-mode.test.ts +68 -0
  38. package/src/theme/__tests__/defaults.test.ts +47 -0
  39. package/src/theme/__tests__/resolve.test.ts +61 -0
  40. package/src/theme/dark-mode.ts +123 -0
  41. package/src/theme/defaults.ts +85 -0
  42. package/src/theme/index.ts +7 -0
  43. package/src/theme/resolve.ts +190 -0
  44. package/src/types/__tests__/spec.test.ts +387 -0
  45. package/src/types/encoding.ts +144 -0
  46. package/src/types/events.ts +96 -0
  47. package/src/types/index.ts +141 -0
  48. package/src/types/layout.ts +794 -0
  49. package/src/types/spec.ts +563 -0
  50. package/src/types/table.ts +105 -0
  51. package/src/types/theme.ts +159 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Spec construction helpers: typed builder functions for common chart types.
3
+ *
4
+ * These builders reduce boilerplate when creating specs programmatically.
5
+ * They accept field names as strings (simple case) or full EncodingChannel
6
+ * objects (when you need to customize type, aggregate, axis, or scale).
7
+ *
8
+ * Type inference: when a string field name is provided, the builder samples
9
+ * data values to infer the encoding type (quantitative, temporal, nominal).
10
+ */
11
+
12
+ import type {
13
+ Annotation,
14
+ ChartSpec,
15
+ ChartType,
16
+ Chrome,
17
+ DarkMode,
18
+ DataRow,
19
+ Encoding,
20
+ EncodingChannel,
21
+ FieldType,
22
+ TableSpec,
23
+ ThemeConfig,
24
+ } from '../types/spec';
25
+ import type { ColumnConfig } from '../types/table';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** A field reference: either a plain string (field name) or a full EncodingChannel. */
32
+ export type FieldRef = string | EncodingChannel;
33
+
34
+ /** Common options shared by all chart builder functions. */
35
+ export interface ChartBuilderOptions {
36
+ /** Color encoding: field name or full channel config for series differentiation. */
37
+ color?: FieldRef;
38
+ /** Size encoding: field name or full channel config. */
39
+ size?: FieldRef;
40
+ /** Editorial chrome (title, subtitle, source, etc.). */
41
+ chrome?: Chrome;
42
+ /** Data annotations. */
43
+ annotations?: Annotation[];
44
+ /** Whether the chart adapts to container width. Defaults to true. */
45
+ responsive?: boolean;
46
+ /** Theme configuration overrides. */
47
+ theme?: ThemeConfig;
48
+ /** Dark mode behavior. */
49
+ darkMode?: DarkMode;
50
+ }
51
+
52
+ /** Options for the dataTable builder. */
53
+ export interface TableBuilderOptions {
54
+ /** Column definitions. Auto-generated from data keys if omitted. */
55
+ columns?: ColumnConfig[];
56
+ /** Field to use as a unique row identifier. */
57
+ rowKey?: string;
58
+ /** Editorial chrome. */
59
+ chrome?: Chrome;
60
+ /** Theme configuration overrides. */
61
+ theme?: ThemeConfig;
62
+ /** Dark mode behavior. */
63
+ darkMode?: DarkMode;
64
+ /** Enable client-side search/filter. */
65
+ search?: boolean;
66
+ /** Pagination configuration. */
67
+ pagination?: boolean | { pageSize: number };
68
+ /** Stick the first column during horizontal scroll. */
69
+ stickyFirstColumn?: boolean;
70
+ /** Compact mode. */
71
+ compact?: boolean;
72
+ /** Whether the table adapts to container width. */
73
+ responsive?: boolean;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Type inference
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /** ISO 8601 date patterns we recognize for temporal inference. */
81
+ const ISO_DATE_RE = /^\d{4}(-\d{2}(-\d{2}(T\d{2}(:\d{2}(:\d{2})?)?)?)?)?$/;
82
+
83
+ /**
84
+ * Infer the encoding field type from data values.
85
+ *
86
+ * Samples up to 20 values from the data array for the given field.
87
+ * - If all sampled values are numbers (or null/undefined), returns "quantitative".
88
+ * - If all sampled string values match ISO date patterns, returns "temporal".
89
+ * - Otherwise returns "nominal".
90
+ */
91
+ export function inferFieldType(data: DataRow[], field: string): FieldType {
92
+ const sampleSize = Math.min(data.length, 20);
93
+ let hasNumber = false;
94
+ let hasDateString = false;
95
+ let hasOtherString = false;
96
+
97
+ for (let i = 0; i < sampleSize; i++) {
98
+ const value = data[i][field];
99
+
100
+ // Skip null/undefined
101
+ if (value == null) continue;
102
+
103
+ if (typeof value === 'number') {
104
+ hasNumber = true;
105
+ } else if (typeof value === 'string') {
106
+ if (ISO_DATE_RE.test(value) && !Number.isNaN(Date.parse(value))) {
107
+ hasDateString = true;
108
+ } else {
109
+ hasOtherString = true;
110
+ }
111
+ } else if (value instanceof Date) {
112
+ hasDateString = true;
113
+ } else {
114
+ hasOtherString = true;
115
+ }
116
+ }
117
+
118
+ // Numbers-only: quantitative
119
+ if (hasNumber && !hasDateString && !hasOtherString) return 'quantitative';
120
+ // Date strings/Date objects only: temporal
121
+ if (hasDateString && !hasNumber && !hasOtherString) return 'temporal';
122
+ // Everything else: nominal
123
+ return 'nominal';
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Internal helpers
128
+ // ---------------------------------------------------------------------------
129
+
130
+ /**
131
+ * Resolve a FieldRef into a full EncodingChannel.
132
+ * If the ref is a string, infer the type from data.
133
+ */
134
+ function resolveField(ref: FieldRef, data: DataRow[]): EncodingChannel {
135
+ if (typeof ref === 'string') {
136
+ return { field: ref, type: inferFieldType(data, ref) };
137
+ }
138
+ return ref;
139
+ }
140
+
141
+ /** Build the encoding object from resolved channels. */
142
+ function buildEncoding(
143
+ channels: { x?: EncodingChannel; y?: EncodingChannel },
144
+ options?: ChartBuilderOptions,
145
+ data?: DataRow[],
146
+ ): Encoding {
147
+ const encoding: Encoding = { ...channels };
148
+ if (options?.color && data) {
149
+ encoding.color = resolveField(options.color, data);
150
+ }
151
+ if (options?.size && data) {
152
+ encoding.size = resolveField(options.size, data);
153
+ }
154
+ return encoding;
155
+ }
156
+
157
+ /** Build a ChartSpec from the resolved pieces. */
158
+ function buildChartSpec(
159
+ type: ChartType,
160
+ data: DataRow[],
161
+ encoding: Encoding,
162
+ options?: ChartBuilderOptions,
163
+ ): ChartSpec {
164
+ const spec: ChartSpec = { type, data, encoding };
165
+ if (options?.chrome) spec.chrome = options.chrome;
166
+ if (options?.annotations) spec.annotations = options.annotations;
167
+ if (options?.responsive !== undefined) spec.responsive = options.responsive;
168
+ if (options?.theme) spec.theme = options.theme;
169
+ if (options?.darkMode) spec.darkMode = options.darkMode;
170
+ return spec;
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Builder functions
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /**
178
+ * Create a line chart spec.
179
+ *
180
+ * @param data - Array of data rows.
181
+ * @param x - X-axis field (typically temporal or ordinal).
182
+ * @param y - Y-axis field (typically quantitative).
183
+ * @param options - Optional color, size, chrome, annotations, theme, etc.
184
+ */
185
+ export function lineChart(
186
+ data: DataRow[],
187
+ x: FieldRef,
188
+ y: FieldRef,
189
+ options?: ChartBuilderOptions,
190
+ ): ChartSpec {
191
+ const xChannel = resolveField(x, data);
192
+ const yChannel = resolveField(y, data);
193
+ const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
194
+ return buildChartSpec('line', data, encoding, options);
195
+ }
196
+
197
+ /**
198
+ * Create a bar chart spec (horizontal bars).
199
+ *
200
+ * Convention: category goes on y-axis, value on x-axis.
201
+ * The `category` param maps to y, `value` maps to x.
202
+ *
203
+ * @param data - Array of data rows.
204
+ * @param category - Category field (y-axis, nominal/ordinal).
205
+ * @param value - Value field (x-axis, quantitative).
206
+ * @param options - Optional color, size, chrome, annotations, theme, etc.
207
+ */
208
+ export function barChart(
209
+ data: DataRow[],
210
+ category: FieldRef,
211
+ value: FieldRef,
212
+ options?: ChartBuilderOptions,
213
+ ): ChartSpec {
214
+ const categoryChannel = resolveField(category, data);
215
+ const valueChannel = resolveField(value, data);
216
+
217
+ // Bar charts: category on y, value on x
218
+ const encoding = buildEncoding({ x: valueChannel, y: categoryChannel }, options, data);
219
+ return buildChartSpec('bar', data, encoding, options);
220
+ }
221
+
222
+ /**
223
+ * Create a column chart spec (vertical columns).
224
+ *
225
+ * @param data - Array of data rows.
226
+ * @param x - X-axis field (category or temporal).
227
+ * @param y - Y-axis field (quantitative value).
228
+ * @param options - Optional color, size, chrome, annotations, theme, etc.
229
+ */
230
+ export function columnChart(
231
+ data: DataRow[],
232
+ x: FieldRef,
233
+ y: FieldRef,
234
+ options?: ChartBuilderOptions,
235
+ ): ChartSpec {
236
+ const xChannel = resolveField(x, data);
237
+ const yChannel = resolveField(y, data);
238
+ const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
239
+ return buildChartSpec('column', data, encoding, options);
240
+ }
241
+
242
+ /**
243
+ * Create a pie chart spec.
244
+ *
245
+ * Convention: category maps to the color channel, value maps to y.
246
+ * Pie charts have no x-axis.
247
+ *
248
+ * @param data - Array of data rows.
249
+ * @param category - Category field (color channel, nominal).
250
+ * @param value - Value field (y channel, quantitative).
251
+ * @param options - Optional chrome, annotations, theme, etc.
252
+ * Note: color option is ignored since category is used for color.
253
+ */
254
+ export function pieChart(
255
+ data: DataRow[],
256
+ category: FieldRef,
257
+ value: FieldRef,
258
+ options?: ChartBuilderOptions,
259
+ ): ChartSpec {
260
+ const categoryChannel = resolveField(category, data);
261
+ const valueChannel = resolveField(value, data);
262
+
263
+ // Pie charts: value on y, category on color (no x-axis)
264
+ const encoding: Encoding = {
265
+ y: valueChannel,
266
+ color: categoryChannel,
267
+ };
268
+ if (options?.size && data) {
269
+ encoding.size = resolveField(options.size, data);
270
+ }
271
+
272
+ return buildChartSpec('pie', data, encoding, options);
273
+ }
274
+
275
+ /**
276
+ * Create an area chart spec.
277
+ *
278
+ * @param data - Array of data rows.
279
+ * @param x - X-axis field (typically temporal or ordinal).
280
+ * @param y - Y-axis field (typically quantitative).
281
+ * @param options - Optional color, size, chrome, annotations, theme, etc.
282
+ */
283
+ export function areaChart(
284
+ data: DataRow[],
285
+ x: FieldRef,
286
+ y: FieldRef,
287
+ options?: ChartBuilderOptions,
288
+ ): ChartSpec {
289
+ const xChannel = resolveField(x, data);
290
+ const yChannel = resolveField(y, data);
291
+ const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
292
+ return buildChartSpec('area', data, encoding, options);
293
+ }
294
+
295
+ /**
296
+ * Create a donut chart spec.
297
+ *
298
+ * Convention: category maps to the color channel, value maps to y.
299
+ * Donut charts have no x-axis.
300
+ *
301
+ * @param data - Array of data rows.
302
+ * @param category - Category field (color channel, nominal).
303
+ * @param value - Value field (y channel, quantitative).
304
+ * @param options - Optional chrome, annotations, theme, etc.
305
+ * Note: color option is ignored since category is used for color.
306
+ */
307
+ export function donutChart(
308
+ data: DataRow[],
309
+ category: FieldRef,
310
+ value: FieldRef,
311
+ options?: ChartBuilderOptions,
312
+ ): ChartSpec {
313
+ const categoryChannel = resolveField(category, data);
314
+ const valueChannel = resolveField(value, data);
315
+
316
+ const encoding: Encoding = {
317
+ y: valueChannel,
318
+ color: categoryChannel,
319
+ };
320
+ if (options?.size && data) {
321
+ encoding.size = resolveField(options.size, data);
322
+ }
323
+
324
+ return buildChartSpec('donut', data, encoding, options);
325
+ }
326
+
327
+ /**
328
+ * Create a dot chart spec (strip plot / dot plot).
329
+ *
330
+ * @param data - Array of data rows.
331
+ * @param x - X-axis field (quantitative or temporal).
332
+ * @param y - Y-axis field (nominal/categorical grouping).
333
+ * @param options - Optional color, size, chrome, annotations, theme, etc.
334
+ */
335
+ export function dotChart(
336
+ data: DataRow[],
337
+ x: FieldRef,
338
+ y: FieldRef,
339
+ options?: ChartBuilderOptions,
340
+ ): ChartSpec {
341
+ const xChannel = resolveField(x, data);
342
+ const yChannel = resolveField(y, data);
343
+ const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
344
+ return buildChartSpec('dot', data, encoding, options);
345
+ }
346
+
347
+ /**
348
+ * Create a scatter chart spec.
349
+ *
350
+ * @param data - Array of data rows.
351
+ * @param x - X-axis field (quantitative).
352
+ * @param y - Y-axis field (quantitative).
353
+ * @param options - Optional color, size, chrome, annotations, theme, etc.
354
+ */
355
+ export function scatterChart(
356
+ data: DataRow[],
357
+ x: FieldRef,
358
+ y: FieldRef,
359
+ options?: ChartBuilderOptions,
360
+ ): ChartSpec {
361
+ const xChannel = resolveField(x, data);
362
+ const yChannel = resolveField(y, data);
363
+ const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
364
+ return buildChartSpec('scatter', data, encoding, options);
365
+ }
366
+
367
+ /**
368
+ * Create a data table spec.
369
+ *
370
+ * If no columns are specified, auto-generates column configs from the
371
+ * keys of the first data row.
372
+ *
373
+ * @param data - Array of data rows.
374
+ * @param options - Column definitions, chrome, pagination, search, etc.
375
+ */
376
+ export function dataTable(data: DataRow[], options?: TableBuilderOptions): TableSpec {
377
+ // Auto-generate columns from data keys if not provided
378
+ let columns = options?.columns;
379
+ if (!columns && data.length > 0) {
380
+ columns = Object.keys(data[0]).map((key): ColumnConfig => {
381
+ // Infer alignment from data type
382
+ const fieldType = inferFieldType(data, key);
383
+ const align = fieldType === 'quantitative' ? ('right' as const) : ('left' as const);
384
+
385
+ return {
386
+ key,
387
+ label: key,
388
+ align,
389
+ };
390
+ });
391
+ }
392
+
393
+ const spec: TableSpec = {
394
+ type: 'table',
395
+ data,
396
+ columns: columns ?? [],
397
+ };
398
+
399
+ if (options?.rowKey) spec.rowKey = options.rowKey;
400
+ if (options?.chrome) spec.chrome = options.chrome;
401
+ if (options?.theme) spec.theme = options.theme;
402
+ if (options?.darkMode) spec.darkMode = options.darkMode;
403
+ if (options?.search !== undefined) spec.search = options.search;
404
+ if (options?.pagination !== undefined) spec.pagination = options.pagination;
405
+ if (options?.stickyFirstColumn !== undefined) spec.stickyFirstColumn = options.stickyFirstColumn;
406
+ if (options?.compact !== undefined) spec.compact = options.compact;
407
+ if (options?.responsive !== undefined) spec.responsive = options.responsive;
408
+
409
+ return spec;
410
+ }
package/src/index.ts ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @opendata-ai/openchart-core
3
+ *
4
+ * Core types, theme engine, color system, accessibility, and locale utilities
5
+ * for the openchart library.
6
+ *
7
+ * This package has no DOM dependencies and runs in any JavaScript environment.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Type system (specs, layouts, marks, encoding, theme types)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export * from './types/index';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Colors: palette collections, contrast utilities, color-blindness simulation
18
+ //
19
+ // Individual named palettes (SEQUENTIAL_BLUE, DIVERGING_RED_BLUE, etc.) are
20
+ // available via SEQUENTIAL_PALETTES and DIVERGING_PALETTES or by direct import
21
+ // from '@opendata-ai/openchart-core/src/colors/palettes'.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export type {
25
+ CategoricalPalette,
26
+ ColorBlindnessType,
27
+ DivergingPalette,
28
+ SequentialPalette,
29
+ } from './colors/index';
30
+ export {
31
+ CATEGORICAL_PALETTE,
32
+ checkPaletteDistinguishability,
33
+ contrastRatio,
34
+ DIVERGING_PALETTES,
35
+ findAccessibleColor,
36
+ meetsAA,
37
+ SEQUENTIAL_PALETTES,
38
+ simulateColorBlindness,
39
+ } from './colors/index';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Theme: defaults, resolution, dark-mode adaptation
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export {
46
+ adaptColorForDarkMode,
47
+ adaptTheme,
48
+ DEFAULT_THEME,
49
+ resolveTheme,
50
+ } from './theme/index';
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Layout: text measurement, chrome computation
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export {
57
+ computeChrome,
58
+ estimateTextWidth,
59
+ } from './layout/index';
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Responsive: breakpoints and layout strategies
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export type {
66
+ AnnotationPosition,
67
+ AxisLabelDensity,
68
+ Breakpoint,
69
+ LabelMode,
70
+ LayoutStrategy,
71
+ LegendPosition,
72
+ } from './responsive/index';
73
+ export {
74
+ getBreakpoint,
75
+ getLayoutStrategy,
76
+ } from './responsive/index';
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Labels: collision detection and resolution
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export type {
83
+ LabelCandidate,
84
+ LabelPriority,
85
+ } from './labels/index';
86
+ export { resolveCollisions } from './labels/index';
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Locale: number and date formatting
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export type { DateGranularity } from './locale/index';
93
+ export {
94
+ abbreviateNumber,
95
+ formatDate,
96
+ formatNumber,
97
+ } from './locale/index';
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Accessibility: alt text and ARIA label generation
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export {
104
+ generateAltText,
105
+ generateAriaLabels,
106
+ generateDataTable,
107
+ } from './accessibility/index';
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Helpers: spec construction builders
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export type {
114
+ ChartBuilderOptions,
115
+ FieldRef,
116
+ TableBuilderOptions,
117
+ } from './helpers/spec-builders';
118
+ export {
119
+ areaChart,
120
+ barChart,
121
+ columnChart,
122
+ dataTable,
123
+ donutChart,
124
+ dotChart,
125
+ inferFieldType,
126
+ lineChart,
127
+ pieChart,
128
+ scatterChart,
129
+ } from './helpers/spec-builders';