@opendata-ai/openchart-engine 6.0.0 → 6.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.
Files changed (65) hide show
  1. package/dist/index.d.ts +155 -19
  2. package/dist/index.js +1513 -164
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/src/__test-fixtures__/specs.ts +6 -3
  6. package/src/__tests__/axes.test.ts +168 -4
  7. package/src/__tests__/compile-chart.test.ts +23 -12
  8. package/src/__tests__/compile-layer.test.ts +386 -0
  9. package/src/__tests__/dimensions.test.ts +6 -3
  10. package/src/__tests__/legend.test.ts +6 -3
  11. package/src/__tests__/scales.test.ts +176 -2
  12. package/src/annotations/__tests__/compute.test.ts +8 -4
  13. package/src/charts/bar/__tests__/compute.test.ts +12 -6
  14. package/src/charts/bar/compute.ts +21 -5
  15. package/src/charts/column/__tests__/compute.test.ts +14 -7
  16. package/src/charts/column/compute.ts +21 -6
  17. package/src/charts/dot/__tests__/compute.test.ts +10 -5
  18. package/src/charts/dot/compute.ts +10 -4
  19. package/src/charts/line/__tests__/compute.test.ts +102 -11
  20. package/src/charts/line/__tests__/curves.test.ts +51 -0
  21. package/src/charts/line/__tests__/labels.test.ts +2 -1
  22. package/src/charts/line/__tests__/mark-options.test.ts +175 -0
  23. package/src/charts/line/area.ts +19 -8
  24. package/src/charts/line/compute.ts +64 -25
  25. package/src/charts/line/curves.ts +40 -0
  26. package/src/charts/pie/__tests__/compute.test.ts +10 -5
  27. package/src/charts/pie/compute.ts +2 -1
  28. package/src/charts/rule/index.ts +127 -0
  29. package/src/charts/scatter/__tests__/compute.test.ts +10 -5
  30. package/src/charts/scatter/compute.ts +15 -5
  31. package/src/charts/text/index.ts +92 -0
  32. package/src/charts/tick/index.ts +84 -0
  33. package/src/charts/utils.ts +1 -1
  34. package/src/compile.ts +175 -23
  35. package/src/compiler/__tests__/compile.test.ts +4 -4
  36. package/src/compiler/__tests__/normalize.test.ts +4 -4
  37. package/src/compiler/__tests__/validate.test.ts +25 -26
  38. package/src/compiler/index.ts +1 -1
  39. package/src/compiler/normalize.ts +77 -4
  40. package/src/compiler/types.ts +6 -2
  41. package/src/compiler/validate.ts +167 -35
  42. package/src/graphs/__tests__/compile-graph.test.ts +2 -2
  43. package/src/graphs/compile-graph.ts +2 -2
  44. package/src/index.ts +17 -1
  45. package/src/layout/axes.ts +122 -20
  46. package/src/layout/dimensions.ts +15 -9
  47. package/src/layout/scales.ts +320 -31
  48. package/src/legend/compute.ts +9 -6
  49. package/src/tables/__tests__/compile-table.test.ts +1 -1
  50. package/src/tooltips/__tests__/compute.test.ts +10 -5
  51. package/src/tooltips/compute.ts +32 -14
  52. package/src/transforms/__tests__/bin.test.ts +88 -0
  53. package/src/transforms/__tests__/calculate.test.ts +146 -0
  54. package/src/transforms/__tests__/conditional.test.ts +109 -0
  55. package/src/transforms/__tests__/filter.test.ts +59 -0
  56. package/src/transforms/__tests__/index.test.ts +93 -0
  57. package/src/transforms/__tests__/predicates.test.ts +176 -0
  58. package/src/transforms/__tests__/timeunit.test.ts +129 -0
  59. package/src/transforms/bin.ts +87 -0
  60. package/src/transforms/calculate.ts +60 -0
  61. package/src/transforms/conditional.ts +46 -0
  62. package/src/transforms/filter.ts +17 -0
  63. package/src/transforms/index.ts +48 -0
  64. package/src/transforms/predicates.ts +90 -0
  65. package/src/transforms/timeunit.ts +88 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-engine",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@opendata-ai/openchart-core": "6.0.0",
48
+ "@opendata-ai/openchart-core": "workspace:*",
49
49
  "d3-array": "^3.2.0",
50
50
  "d3-format": "^3.1.2",
51
51
  "d3-interpolate": "^3.0.0",
@@ -48,7 +48,8 @@ export function makeCompactStrategy(): LayoutStrategy {
48
48
  /** Single-series line chart with temporal x-axis. */
49
49
  export function makeLineSpec(): NormalizedChartSpec {
50
50
  return {
51
- type: 'line',
51
+ markType: 'line',
52
+ markDef: { type: 'line' },
52
53
  data: [
53
54
  { date: '2020-01-01', value: 10, country: 'US' },
54
55
  { date: '2021-01-01', value: 40, country: 'US' },
@@ -82,7 +83,8 @@ export function makeLineSpec(): NormalizedChartSpec {
82
83
  /** Basic bar chart (horizontal) with nominal y and quantitative x. */
83
84
  export function makeBarSpec(): NormalizedChartSpec {
84
85
  return {
85
- type: 'bar',
86
+ markType: 'bar',
87
+ markDef: { type: 'bar' },
86
88
  data: [
87
89
  { name: 'A', value: 10 },
88
90
  { name: 'B', value: 30 },
@@ -110,7 +112,8 @@ export function makeBarSpec(): NormalizedChartSpec {
110
112
  /** Basic scatter chart with quantitative x and y. */
111
113
  export function makeScatterSpec(): NormalizedChartSpec {
112
114
  return {
113
- type: 'scatter',
115
+ markType: 'point',
116
+ markDef: { type: 'point' },
114
117
  data: [
115
118
  { x: 10, y: 20 },
116
119
  { x: 30, y: 50 },
@@ -6,7 +6,8 @@ import { computeAxes, effectiveDensity, thinTicksUntilFit, ticksOverlap } from '
6
6
  import { computeScales } from '../layout/scales';
7
7
 
8
8
  const lineSpec: NormalizedChartSpec = {
9
- type: 'line',
9
+ markType: 'line',
10
+ markDef: { type: 'line' },
10
11
  data: [
11
12
  { date: '2020-01-01', value: 100 },
12
13
  { date: '2021-01-01', value: 500 },
@@ -208,7 +209,8 @@ describe('computeAxes', () => {
208
209
  it('propagates tickAngle from encoding to x-axis layout', () => {
209
210
  const specWithAngle: NormalizedChartSpec = {
210
211
  ...lineSpec,
211
- type: 'column',
212
+ markType: 'bar',
213
+ markDef: { type: 'bar', orient: 'vertical' },
212
214
  data: [
213
215
  { cat: 'California', val: 10 },
214
216
  { cat: 'New York', val: 20 },
@@ -439,7 +441,8 @@ describe('text-aware tick density', () => {
439
441
  const categories = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo'];
440
442
  const barSpec: NormalizedChartSpec = {
441
443
  ...lineSpec,
442
- type: 'column',
444
+ markType: 'bar',
445
+ markDef: { type: 'bar', orient: 'vertical' },
443
446
  data: categories.map((cat, i) => ({ cat, val: (i + 1) * 10 })),
444
447
  encoding: {
445
448
  x: { field: 'cat', type: 'nominal' },
@@ -490,7 +493,8 @@ describe('text-aware tick density', () => {
490
493
  it('passes measureText to auto-rotation detection', () => {
491
494
  const barSpec: NormalizedChartSpec = {
492
495
  ...lineSpec,
493
- type: 'column',
496
+ markType: 'bar',
497
+ markDef: { type: 'bar', orient: 'vertical' },
494
498
  data: [
495
499
  { cat: 'A', val: 10 },
496
500
  { cat: 'B', val: 20 },
@@ -509,3 +513,163 @@ describe('text-aware tick density', () => {
509
513
  expect(axes.x!.tickAngle).toBe(-45);
510
514
  });
511
515
  });
516
+
517
+ // ---------------------------------------------------------------------------
518
+ // Axis config expansion tests
519
+ // ---------------------------------------------------------------------------
520
+
521
+ describe('axis config properties', () => {
522
+ it('uses title instead of deprecated label', () => {
523
+ const spec: NormalizedChartSpec = {
524
+ ...lineSpec,
525
+ encoding: {
526
+ x: { field: 'date', type: 'temporal', axis: { title: 'Year' } },
527
+ y: { field: 'value', type: 'quantitative', axis: { title: 'Amount ($)' } },
528
+ },
529
+ };
530
+ const scales = computeScales(spec, chartArea, spec.data);
531
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
532
+
533
+ expect(axes.x!.label).toBe('Year');
534
+ expect(axes.y!.label).toBe('Amount ($)');
535
+ });
536
+
537
+ it('falls back to deprecated label when title is not set', () => {
538
+ const spec: NormalizedChartSpec = {
539
+ ...lineSpec,
540
+ encoding: {
541
+ x: { field: 'date', type: 'temporal', axis: { label: 'Old Label' } },
542
+ y: { field: 'value', type: 'quantitative' },
543
+ },
544
+ };
545
+ const scales = computeScales(spec, chartArea, spec.data);
546
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
547
+
548
+ expect(axes.x!.label).toBe('Old Label');
549
+ });
550
+
551
+ it('prefers labelAngle over deprecated tickAngle', () => {
552
+ const spec: NormalizedChartSpec = {
553
+ ...lineSpec,
554
+ encoding: {
555
+ x: {
556
+ field: 'date',
557
+ type: 'temporal',
558
+ axis: { labelAngle: -30, tickAngle: -90 },
559
+ },
560
+ y: { field: 'value', type: 'quantitative' },
561
+ },
562
+ };
563
+ const scales = computeScales(spec, chartArea, spec.data);
564
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
565
+
566
+ // labelAngle takes precedence
567
+ expect(axes.x!.tickAngle).toBe(-30);
568
+ });
569
+
570
+ it('passes orient config to axis layout', () => {
571
+ const spec: NormalizedChartSpec = {
572
+ ...lineSpec,
573
+ encoding: {
574
+ x: { field: 'date', type: 'temporal', axis: { orient: 'top' } },
575
+ y: { field: 'value', type: 'quantitative', axis: { orient: 'right' } },
576
+ },
577
+ };
578
+ const scales = computeScales(spec, chartArea, spec.data);
579
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
580
+
581
+ expect(axes.x!.orient).toBe('top');
582
+ expect(axes.y!.orient).toBe('right');
583
+ });
584
+
585
+ it('passes domain and ticks visibility to axis layout', () => {
586
+ const spec: NormalizedChartSpec = {
587
+ ...lineSpec,
588
+ encoding: {
589
+ x: { field: 'date', type: 'temporal', axis: { domain: false, ticks: false } },
590
+ y: { field: 'value', type: 'quantitative' },
591
+ },
592
+ };
593
+ const scales = computeScales(spec, chartArea, spec.data);
594
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
595
+
596
+ expect(axes.x!.domainLine).toBe(false);
597
+ expect(axes.x!.tickMarks).toBe(false);
598
+ });
599
+
600
+ it('passes offset and padding configs to axis layout', () => {
601
+ const spec: NormalizedChartSpec = {
602
+ ...lineSpec,
603
+ encoding: {
604
+ x: {
605
+ field: 'date',
606
+ type: 'temporal',
607
+ axis: { offset: 10, titlePadding: 8, labelPadding: 4 },
608
+ },
609
+ y: { field: 'value', type: 'quantitative' },
610
+ },
611
+ };
612
+ const scales = computeScales(spec, chartArea, spec.data);
613
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
614
+
615
+ expect(axes.x!.offset).toBe(10);
616
+ expect(axes.x!.titlePadding).toBe(8);
617
+ expect(axes.x!.labelPadding).toBe(4);
618
+ });
619
+
620
+ it('passes labelOverlap and labelFlush to axis layout', () => {
621
+ const spec: NormalizedChartSpec = {
622
+ ...lineSpec,
623
+ encoding: {
624
+ x: {
625
+ field: 'date',
626
+ type: 'temporal',
627
+ axis: { labelOverlap: 'parity', labelFlush: true },
628
+ },
629
+ y: { field: 'value', type: 'quantitative' },
630
+ },
631
+ };
632
+ const scales = computeScales(spec, chartArea, spec.data);
633
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
634
+
635
+ expect(axes.x!.labelOverlap).toBe('parity');
636
+ expect(axes.x!.labelFlush).toBe(true);
637
+ });
638
+
639
+ it('uses explicit tick values when provided', () => {
640
+ const spec: NormalizedChartSpec = {
641
+ ...lineSpec,
642
+ encoding: {
643
+ x: { field: 'date', type: 'temporal' },
644
+ y: {
645
+ field: 'value',
646
+ type: 'quantitative',
647
+ axis: { values: [0, 250, 500] },
648
+ },
649
+ },
650
+ };
651
+ const scales = computeScales(spec, chartArea, spec.data);
652
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
653
+
654
+ // Should produce exactly 3 ticks matching our explicit values
655
+ expect(axes.y!.ticks.length).toBe(3);
656
+ expect(axes.y!.ticks[0].value).toBe(0);
657
+ expect(axes.y!.ticks[1].value).toBe(250);
658
+ expect(axes.y!.ticks[2].value).toBe(500);
659
+ });
660
+
661
+ it('defaults are undefined when axis config properties are not set', () => {
662
+ const scales = computeScales(lineSpec, chartArea, lineSpec.data);
663
+ const axes = computeAxes(scales, chartArea, fullStrategy, theme);
664
+
665
+ // All new properties should be undefined when not set
666
+ expect(axes.x!.orient).toBeUndefined();
667
+ expect(axes.x!.domainLine).toBeUndefined();
668
+ expect(axes.x!.tickMarks).toBeUndefined();
669
+ expect(axes.x!.offset).toBeUndefined();
670
+ expect(axes.x!.titlePadding).toBeUndefined();
671
+ expect(axes.x!.labelPadding).toBeUndefined();
672
+ expect(axes.x!.labelOverlap).toBeUndefined();
673
+ expect(axes.x!.labelFlush).toBeUndefined();
674
+ });
675
+ });
@@ -6,7 +6,7 @@ import { compileChart, compileGraph, compileTable } from '../compile';
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
8
  const lineSpec = {
9
- type: 'line' as const,
9
+ mark: 'line' as const,
10
10
  data: [
11
11
  { date: '2020-01-01', value: 10, country: 'US' },
12
12
  { date: '2021-01-01', value: 40, country: 'US' },
@@ -26,7 +26,7 @@ const lineSpec = {
26
26
  };
27
27
 
28
28
  const barSpec = {
29
- type: 'bar' as const,
29
+ mark: 'bar' as const,
30
30
  data: [
31
31
  { name: 'A', value: 10 },
32
32
  { name: 'B', value: 30 },
@@ -172,25 +172,36 @@ describe('compileChart', () => {
172
172
  expect(layout.legend.position).toBe('top');
173
173
  });
174
174
 
175
- it('produces line and point marks with the registered renderer', () => {
175
+ it('produces line marks with dataPoints (no PointMarks by default)', () => {
176
176
  const layout = compileChart(lineSpec, { width: 600, height: 400 });
177
177
  expect(layout.marks.length).toBeGreaterThan(0);
178
178
 
179
179
  const lineMarks = layout.marks.filter((m) => m.type === 'line');
180
180
  const pointMarks = layout.marks.filter((m) => m.type === 'point');
181
181
  expect(lineMarks.length).toBeGreaterThan(0);
182
- expect(pointMarks.length).toBeGreaterThan(0);
182
+ // Default: no PointMarks (voronoi overlay handles tooltips)
183
+ expect(pointMarks.length).toBe(0);
183
184
 
184
- // Line marks should have points with valid coordinates
185
+ // Line marks should have points and dataPoints with valid coordinates
185
186
  for (const mark of lineMarks) {
186
187
  if (mark.type === 'line') {
187
188
  expect(mark.points.length).toBeGreaterThan(0);
188
189
  expect(mark.stroke).toBeTruthy();
189
190
  expect(mark.strokeWidth).toBeGreaterThan(0);
191
+ expect(mark.dataPoints).toBeDefined();
192
+ expect(mark.dataPoints!.length).toBeGreaterThan(0);
190
193
  }
191
194
  }
192
195
  });
193
196
 
197
+ it('produces PointMarks when mark.point is true', () => {
198
+ const specWithPoints = { ...lineSpec, mark: { type: 'line' as const, point: true as const } };
199
+ const layout = compileChart(specWithPoints, { width: 600, height: 400 });
200
+
201
+ const pointMarks = layout.marks.filter((m) => m.type === 'point');
202
+ expect(pointMarks.length).toBeGreaterThan(0);
203
+ });
204
+
194
205
  it('includes accessibility metadata with meaningful content', () => {
195
206
  const layout = compileChart(lineSpec, { width: 600, height: 400 });
196
207
  expect(layout.a11y.altText).toContain('Line chart');
@@ -312,7 +323,7 @@ describe('compileChart', () => {
312
323
 
313
324
  it('scale.clip filters data rows outside the y-axis domain', () => {
314
325
  const spec = {
315
- type: 'scatter' as const,
326
+ mark: 'point' as const,
316
327
  data: [
317
328
  { x: 1, y: 5 },
318
329
  { x: 2, y: 15 },
@@ -337,7 +348,7 @@ describe('compileChart', () => {
337
348
 
338
349
  it('scale.clip filters data rows outside the x-axis domain', () => {
339
350
  const spec = {
340
- type: 'scatter' as const,
351
+ mark: 'point' as const,
341
352
  data: [
342
353
  { x: 1, y: 10 },
343
354
  { x: 5, y: 20 },
@@ -362,7 +373,7 @@ describe('compileChart', () => {
362
373
 
363
374
  it('scale.clip=false does not filter data even with domain set', () => {
364
375
  const spec = {
365
- type: 'scatter' as const,
376
+ mark: 'point' as const,
366
377
  data: [
367
378
  { x: 1, y: 5 },
368
379
  { x: 2, y: 15 },
@@ -446,7 +457,7 @@ describe('compileGraph', () => {
446
457
  expect(() =>
447
458
  compileGraph(
448
459
  {
449
- type: 'scatter',
460
+ mark: 'point',
450
461
  data: [{ x: 1, y: 2 }],
451
462
  encoding: {
452
463
  x: { field: 'x', type: 'quantitative' },
@@ -455,12 +466,12 @@ describe('compileGraph', () => {
455
466
  },
456
467
  { width: 600, height: 400 },
457
468
  ),
458
- ).toThrow('compileGraph received a scatter spec');
469
+ ).toThrow('compileGraph received a non-graph spec');
459
470
  });
460
471
 
461
472
  it('propagates tickAngle through the full compilation pipeline', () => {
462
473
  const columnSpec = {
463
- type: 'column' as const,
474
+ mark: 'bar' as const,
464
475
  data: [
465
476
  { state: 'California', pop: 39000000 },
466
477
  { state: 'Texas', pop: 29000000 },
@@ -484,7 +495,7 @@ describe('compileGraph', () => {
484
495
 
485
496
  it('reserves extra bottom margin for rotated x-axis labels', () => {
486
497
  const baseColumnSpec = {
487
- type: 'column' as const,
498
+ mark: 'bar' as const,
488
499
  data: [
489
500
  { state: 'California', pop: 39000000 },
490
501
  { state: 'Texas', pop: 29000000 },