@opendata-ai/openchart-engine 6.26.0 → 6.27.2
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.
- package/dist/index.d.ts +37 -1
- package/dist/index.js +206 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +33 -0
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +6 -0
- package/src/__tests__/axes.test.ts +101 -3
- package/src/__tests__/compile-chart.test.ts +301 -0
- package/src/annotations/__tests__/compute.test.ts +175 -0
- package/src/annotations/position.ts +37 -1
- package/src/annotations/resolve-range.ts +5 -5
- package/src/charts/bar/__tests__/compute.test.ts +102 -0
- package/src/charts/bar/compute.ts +1 -0
- package/src/charts/line/area.ts +1 -1
- package/src/charts/line/compute.ts +7 -1
- package/src/compile.ts +175 -4
- package/src/compiler/normalize.ts +26 -0
- package/src/compiler/types.ts +38 -0
- package/src/layout/axes/ticks.ts +31 -4
- package/src/layout/axes.ts +18 -4
- package/src/layout/dimensions.ts +77 -2
- package/src/legend/compute.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.27.2",
|
|
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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"typecheck": "tsc --noEmit"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@opendata-ai/openchart-core": "6.
|
|
51
|
+
"@opendata-ai/openchart-core": "6.27.2",
|
|
52
52
|
"d3-array": "^3.2.0",
|
|
53
53
|
"d3-format": "^3.1.2",
|
|
54
54
|
"d3-interpolate": "^3.0.0",
|
|
@@ -74,6 +74,17 @@ export function makeLineSpec(): NormalizedChartSpec {
|
|
|
74
74
|
hiddenSeries: [],
|
|
75
75
|
seriesStyles: {},
|
|
76
76
|
watermark: true,
|
|
77
|
+
display: 'full',
|
|
78
|
+
userExplicit: {
|
|
79
|
+
chrome: false,
|
|
80
|
+
legend: false,
|
|
81
|
+
xAxis: false,
|
|
82
|
+
yAxis: false,
|
|
83
|
+
labels: false,
|
|
84
|
+
animation: false,
|
|
85
|
+
watermark: false,
|
|
86
|
+
crosshair: false,
|
|
87
|
+
},
|
|
77
88
|
};
|
|
78
89
|
}
|
|
79
90
|
|
|
@@ -104,6 +115,17 @@ export function makeBarSpec(): NormalizedChartSpec {
|
|
|
104
115
|
hiddenSeries: [],
|
|
105
116
|
seriesStyles: {},
|
|
106
117
|
watermark: true,
|
|
118
|
+
display: 'full',
|
|
119
|
+
userExplicit: {
|
|
120
|
+
chrome: false,
|
|
121
|
+
legend: false,
|
|
122
|
+
xAxis: false,
|
|
123
|
+
yAxis: false,
|
|
124
|
+
labels: false,
|
|
125
|
+
animation: false,
|
|
126
|
+
watermark: false,
|
|
127
|
+
crosshair: false,
|
|
128
|
+
},
|
|
107
129
|
};
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -136,5 +158,16 @@ export function makeScatterSpec(): NormalizedChartSpec {
|
|
|
136
158
|
hiddenSeries: [],
|
|
137
159
|
seriesStyles: {},
|
|
138
160
|
watermark: true,
|
|
161
|
+
display: 'full',
|
|
162
|
+
userExplicit: {
|
|
163
|
+
chrome: false,
|
|
164
|
+
legend: false,
|
|
165
|
+
xAxis: false,
|
|
166
|
+
yAxis: false,
|
|
167
|
+
labels: false,
|
|
168
|
+
animation: false,
|
|
169
|
+
watermark: false,
|
|
170
|
+
crosshair: false,
|
|
171
|
+
},
|
|
139
172
|
};
|
|
140
173
|
}
|
|
@@ -181,10 +181,12 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
|
|
|
181
181
|
},
|
|
182
182
|
"topHeight": 36.6,
|
|
183
183
|
},
|
|
184
|
+
"crosshair": false,
|
|
184
185
|
"dimensions": {
|
|
185
186
|
"height": 400,
|
|
186
187
|
"width": 600,
|
|
187
188
|
},
|
|
189
|
+
"display": "full",
|
|
188
190
|
"legend": {
|
|
189
191
|
"bounds": {
|
|
190
192
|
"height": 0,
|
|
@@ -731,10 +733,12 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
|
|
|
731
733
|
},
|
|
732
734
|
"topHeight": 61.599999999999994,
|
|
733
735
|
},
|
|
736
|
+
"crosshair": false,
|
|
734
737
|
"dimensions": {
|
|
735
738
|
"height": 500,
|
|
736
739
|
"width": 800,
|
|
737
740
|
},
|
|
741
|
+
"display": "full",
|
|
738
742
|
"legend": {
|
|
739
743
|
"bounds": {
|
|
740
744
|
"height": 85.2,
|
|
@@ -1574,10 +1578,12 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
|
|
|
1574
1578
|
},
|
|
1575
1579
|
"topHeight": 36.6,
|
|
1576
1580
|
},
|
|
1581
|
+
"crosshair": false,
|
|
1577
1582
|
"dimensions": {
|
|
1578
1583
|
"height": 400,
|
|
1579
1584
|
"width": 600,
|
|
1580
1585
|
},
|
|
1586
|
+
"display": "full",
|
|
1581
1587
|
"legend": {
|
|
1582
1588
|
"bounds": {
|
|
1583
1589
|
"height": 0,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { AxisTick, LayoutStrategy } from '@opendata-ai/openchart-core';
|
|
2
2
|
import { resolveTheme } from '@opendata-ai/openchart-core';
|
|
3
|
+
import { scaleLinear, scaleLog } from 'd3-scale';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
5
6
|
import { computeAxes, effectiveDensity, thinTicksUntilFit, ticksOverlap } from '../layout/axes';
|
|
7
|
+
import { buildContinuousTicks } from '../layout/axes/ticks';
|
|
8
|
+
import type { ResolvedScale } from '../layout/scales';
|
|
6
9
|
import { computeScales } from '../layout/scales';
|
|
7
10
|
|
|
8
11
|
const lineSpec: NormalizedChartSpec = {
|
|
@@ -121,16 +124,16 @@ describe('computeAxes', () => {
|
|
|
121
124
|
// Height-aware y-axis tick reduction
|
|
122
125
|
// -------------------------------------------------------------------------
|
|
123
126
|
|
|
124
|
-
it('reduces y-axis ticks for very short chart areas (<
|
|
127
|
+
it('reduces y-axis ticks for very short chart areas (< 80px)', () => {
|
|
125
128
|
const shortArea = { x: 50, y: 50, width: 500, height: 80 };
|
|
126
129
|
const scales = computeScales(lineSpec, shortArea, lineSpec.data);
|
|
127
130
|
const axesShort = computeAxes(scales, shortArea, fullStrategy, theme);
|
|
128
131
|
|
|
129
|
-
//
|
|
132
|
+
// Very short chart area -- tick count clamped to at most 4
|
|
130
133
|
expect(axesShort.y!.ticks.length).toBeLessThanOrEqual(4);
|
|
131
134
|
});
|
|
132
135
|
|
|
133
|
-
it('reduces y-axis ticks for medium-short chart areas (
|
|
136
|
+
it('reduces y-axis ticks for medium-short chart areas (80-100px)', () => {
|
|
134
137
|
const mediumArea = { x: 50, y: 50, width: 500, height: 160 };
|
|
135
138
|
const tallArea = { x: 50, y: 50, width: 500, height: 400 };
|
|
136
139
|
|
|
@@ -884,3 +887,98 @@ describe('horizontal bar y-axis label thinning regression', () => {
|
|
|
884
887
|
expect(axes.x!.ticks.length).toBeLessThan(countries.length);
|
|
885
888
|
});
|
|
886
889
|
});
|
|
890
|
+
|
|
891
|
+
// ---------------------------------------------------------------------------
|
|
892
|
+
// Log scale tick filtering — buildContinuousTicks
|
|
893
|
+
// D3 log scales ignore the count hint and return ticks at every sub-power
|
|
894
|
+
// position. The engine must filter these down to powers of the base only.
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Build a ResolvedScale backed by a D3 log scale, matching what buildLogScale produces.
|
|
899
|
+
* Using a single `base` parameter keeps the D3 scale and channel config in sync,
|
|
900
|
+
* mirroring how they're always derived from the same spec field.
|
|
901
|
+
*/
|
|
902
|
+
function makeLogScale(domain: [number, number], base = 10): ResolvedScale {
|
|
903
|
+
const scale = scaleLog().domain(domain).range([400, 0]);
|
|
904
|
+
scale.base(base);
|
|
905
|
+
return {
|
|
906
|
+
scale,
|
|
907
|
+
type: 'log',
|
|
908
|
+
channel: {
|
|
909
|
+
field: 'value',
|
|
910
|
+
type: 'quantitative',
|
|
911
|
+
scale: base !== 10 ? { base } : undefined,
|
|
912
|
+
},
|
|
913
|
+
} as ResolvedScale;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
describe('buildContinuousTicks — log scale power filtering', () => {
|
|
917
|
+
it('returns only power-of-10 ticks for [5, 25000] at tickCount 5', () => {
|
|
918
|
+
const resolved = makeLogScale([5, 25000]);
|
|
919
|
+
const ticks = buildContinuousTicks(resolved, 5);
|
|
920
|
+
const values = ticks.map((t) => t.value as number);
|
|
921
|
+
// Should be exactly the powers of 10 in domain: 10, 100, 1000, 10000
|
|
922
|
+
expect(values).toEqual([10, 100, 1000, 10000]);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('returns only power-of-10 ticks for [1, 1000000] at tickCount 5', () => {
|
|
926
|
+
const resolved = makeLogScale([1, 1_000_000]);
|
|
927
|
+
const ticks = buildContinuousTicks(resolved, 5);
|
|
928
|
+
const values = ticks.map((t) => t.value as number);
|
|
929
|
+
// Assert invariants rather than exact output: every tick is a power of 10,
|
|
930
|
+
// and we get at least 5 (the domain spans 6 decades).
|
|
931
|
+
for (const v of values) {
|
|
932
|
+
const exp = Math.log10(v);
|
|
933
|
+
expect(Math.abs(exp - Math.round(exp))).toBeLessThan(1e-9);
|
|
934
|
+
}
|
|
935
|
+
expect(values.length).toBeGreaterThanOrEqual(5);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('returns only powers-of-2 for [1, 64] base-2 at tickCount 5', () => {
|
|
939
|
+
const resolved = makeLogScale([1, 64], 2);
|
|
940
|
+
const ticks = buildContinuousTicks(resolved, 5);
|
|
941
|
+
const values = ticks.map((t) => t.value as number);
|
|
942
|
+
expect(values).toEqual([1, 2, 4, 8, 16, 32, 64]);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('handles fractional powers for [0.001, 100] at tickCount 5', () => {
|
|
946
|
+
const resolved = makeLogScale([0.001, 100]);
|
|
947
|
+
const ticks = buildContinuousTicks(resolved, 5);
|
|
948
|
+
const values = ticks.map((t) => t.value as number);
|
|
949
|
+
// Tolerance check prevents floating-point false negatives on 0.001, 0.01, 0.1
|
|
950
|
+
expect(values).toEqual([0.001, 0.01, 0.1, 1, 10, 100]);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it('still produces ticks at tickCount 3 (regression guard)', () => {
|
|
954
|
+
const resolved = makeLogScale([1, 1000]);
|
|
955
|
+
const ticks = buildContinuousTicks(resolved, 3);
|
|
956
|
+
expect(ticks.length).toBeGreaterThanOrEqual(2);
|
|
957
|
+
const values = ticks.map((t) => t.value as number);
|
|
958
|
+
// Every value must be a power of 10
|
|
959
|
+
for (const v of values) {
|
|
960
|
+
const exp = Math.log10(v);
|
|
961
|
+
expect(Math.abs(exp - Math.round(exp))).toBeLessThan(1e-6);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it('does not over-filter linear scales with the same domain', () => {
|
|
966
|
+
const scale = scaleLinear().domain([5, 25000]).range([400, 0]);
|
|
967
|
+
const resolved: ResolvedScale = {
|
|
968
|
+
scale,
|
|
969
|
+
type: 'linear',
|
|
970
|
+
channel: { field: 'value', type: 'quantitative' },
|
|
971
|
+
} as ResolvedScale;
|
|
972
|
+
const ticks = buildContinuousTicks(resolved, 5);
|
|
973
|
+
// Linear scale should return normal D3 ticks — not just power-of-10 values
|
|
974
|
+
expect(ticks.length).toBeGreaterThanOrEqual(4);
|
|
975
|
+
// At least one tick should NOT be a power of 10 (e.g. 5000, 10000, 15000, 20000, 25000)
|
|
976
|
+
const values = ticks.map((t) => t.value as number);
|
|
977
|
+
const nonPowerOf10 = values.filter((v) => {
|
|
978
|
+
if (v <= 0) return true;
|
|
979
|
+
const exp = Math.log10(v);
|
|
980
|
+
return Math.abs(exp - Math.round(exp)) >= 0.01;
|
|
981
|
+
});
|
|
982
|
+
expect(nonPowerOf10.length).toBeGreaterThan(0);
|
|
983
|
+
});
|
|
984
|
+
});
|
|
@@ -398,6 +398,307 @@ describe('compileChart', () => {
|
|
|
398
398
|
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
399
399
|
expect(pointMarks.length).toBe(3);
|
|
400
400
|
});
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// display field + breakpoint override
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
it('display defaults to "full" when not specified', () => {
|
|
407
|
+
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
408
|
+
expect(layout.display).toBe('full');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('display: "sparkline" propagates through to ChartLayout', () => {
|
|
412
|
+
const layout = compileChart(
|
|
413
|
+
{ ...lineSpec, display: 'sparkline' as const },
|
|
414
|
+
{ width: 400, height: 80 },
|
|
415
|
+
);
|
|
416
|
+
expect(layout.display).toBe('sparkline');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('breakpoint override flips display at compact width', () => {
|
|
420
|
+
const spec = {
|
|
421
|
+
...lineSpec,
|
|
422
|
+
overrides: {
|
|
423
|
+
compact: { display: 'sparkline' as const },
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const compactLayout = compileChart(spec, { width: 320, height: 200 });
|
|
428
|
+
expect(compactLayout.display).toBe('sparkline');
|
|
429
|
+
|
|
430
|
+
const desktopLayout = compileChart(spec, { width: 1200, height: 600 });
|
|
431
|
+
expect(desktopLayout.display).toBe('full');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('sparkline mode forces watermark off when not user-explicit', () => {
|
|
435
|
+
const layout = compileChart(
|
|
436
|
+
{ ...lineSpec, display: 'sparkline' as const },
|
|
437
|
+
{ width: 400, height: 80 },
|
|
438
|
+
);
|
|
439
|
+
expect(layout.watermark).toBe(false);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('sparkline mode respects explicit watermark: true', () => {
|
|
443
|
+
const layout = compileChart(
|
|
444
|
+
{ ...lineSpec, display: 'sparkline' as const, watermark: true },
|
|
445
|
+
{ width: 400, height: 80 },
|
|
446
|
+
);
|
|
447
|
+
expect(layout.watermark).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('sparkline mode forces crosshair off when not user-explicit', () => {
|
|
451
|
+
const layout = compileChart(
|
|
452
|
+
{ ...lineSpec, display: 'sparkline' as const },
|
|
453
|
+
{ width: 400, height: 80 },
|
|
454
|
+
);
|
|
455
|
+
expect(layout.crosshair).toBe(false);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('sparkline mode respects explicit crosshair: true', () => {
|
|
459
|
+
const layout = compileChart(
|
|
460
|
+
{ ...lineSpec, display: 'sparkline' as const, crosshair: true },
|
|
461
|
+
{ width: 400, height: 80 },
|
|
462
|
+
);
|
|
463
|
+
expect(layout.crosshair).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
// Sparkline layout profile (dimensions, axes, legend)
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
it('sparkline produces near-edge-to-edge chart area (margins <= 4px per side)', () => {
|
|
471
|
+
const layout = compileChart(
|
|
472
|
+
{ ...lineSpec, chrome: undefined, display: 'sparkline' as const },
|
|
473
|
+
{ width: 400, height: 80 },
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// No chrome, no axes, no legend by default. Mark area should be tight.
|
|
477
|
+
expect(layout.area.x).toBeLessThanOrEqual(4);
|
|
478
|
+
expect(layout.area.y).toBeLessThanOrEqual(4);
|
|
479
|
+
const rightMargin = 400 - (layout.area.x + layout.area.width);
|
|
480
|
+
const bottomMargin = 80 - (layout.area.y + layout.area.height);
|
|
481
|
+
expect(rightMargin).toBeLessThanOrEqual(4);
|
|
482
|
+
expect(bottomMargin).toBeLessThanOrEqual(4);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('sparkline returns no axes by default', () => {
|
|
486
|
+
const layout = compileChart(
|
|
487
|
+
{ ...lineSpec, chrome: undefined, display: 'sparkline' as const },
|
|
488
|
+
{ width: 400, height: 80 },
|
|
489
|
+
);
|
|
490
|
+
expect(layout.axes.x).toBeUndefined();
|
|
491
|
+
expect(layout.axes.y).toBeUndefined();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('sparkline + explicit encoding.x.axis still reserves x-axis', () => {
|
|
495
|
+
const layout = compileChart(
|
|
496
|
+
{
|
|
497
|
+
...lineSpec,
|
|
498
|
+
chrome: undefined,
|
|
499
|
+
display: 'sparkline' as const,
|
|
500
|
+
encoding: {
|
|
501
|
+
...lineSpec.encoding,
|
|
502
|
+
x: { ...lineSpec.encoding.x, axis: { title: 'date' } },
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
{ width: 400, height: 200 },
|
|
506
|
+
);
|
|
507
|
+
expect(layout.axes.x).toBeDefined();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('sparkline + explicit chrome.title still renders chrome', () => {
|
|
511
|
+
const layout = compileChart(
|
|
512
|
+
{
|
|
513
|
+
...lineSpec,
|
|
514
|
+
chrome: { title: 'Q4 Revenue' },
|
|
515
|
+
display: 'sparkline' as const,
|
|
516
|
+
},
|
|
517
|
+
{ width: 400, height: 200 },
|
|
518
|
+
);
|
|
519
|
+
expect(layout.chrome.title).toBeDefined();
|
|
520
|
+
expect(layout.chrome.title?.text).toBe('Q4 Revenue');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('sparkline hides legend by default even with color encoding', () => {
|
|
524
|
+
const layout = compileChart(
|
|
525
|
+
{
|
|
526
|
+
...lineSpec,
|
|
527
|
+
chrome: undefined,
|
|
528
|
+
display: 'sparkline' as const,
|
|
529
|
+
},
|
|
530
|
+
{ width: 400, height: 80 },
|
|
531
|
+
);
|
|
532
|
+
expect('entries' in layout.legend && layout.legend.entries.length).toBe(0);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('sparkline + explicit legend.show: true renders legend', () => {
|
|
536
|
+
const layout = compileChart(
|
|
537
|
+
{
|
|
538
|
+
...lineSpec,
|
|
539
|
+
chrome: undefined,
|
|
540
|
+
display: 'sparkline' as const,
|
|
541
|
+
legend: { show: true },
|
|
542
|
+
},
|
|
543
|
+
{ width: 400, height: 200 },
|
|
544
|
+
);
|
|
545
|
+
expect('entries' in layout.legend && layout.legend.entries.length).toBeGreaterThan(0);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('sparkline works at heights as low as 30px', () => {
|
|
549
|
+
const layout = compileChart(
|
|
550
|
+
{ ...lineSpec, chrome: undefined, display: 'sparkline' as const },
|
|
551
|
+
{ width: 200, height: 30 },
|
|
552
|
+
);
|
|
553
|
+
expect(layout.area.height).toBeGreaterThan(0);
|
|
554
|
+
expect(layout.area.width).toBeGreaterThan(0);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('chrome: {} does not count as user-explicit chrome (still stripped in sparkline)', () => {
|
|
558
|
+
// Empty chrome object is the idiom for "silence defaults" — should not
|
|
559
|
+
// opt-in to chrome rendering in sparkline mode.
|
|
560
|
+
const layout = compileChart(
|
|
561
|
+
{ ...lineSpec, chrome: {}, display: 'sparkline' as const },
|
|
562
|
+
{ width: 400, height: 80 },
|
|
563
|
+
);
|
|
564
|
+
expect(layout.chrome.title).toBeUndefined();
|
|
565
|
+
expect(layout.chrome.topHeight).toBe(0);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
// Explicit-at-any-level wins (precedence matrix)
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
it('top-level animation: true wins even when breakpoint flips to sparkline', () => {
|
|
573
|
+
const spec = {
|
|
574
|
+
...lineSpec,
|
|
575
|
+
animation: true as const,
|
|
576
|
+
overrides: { compact: { display: 'sparkline' as const } },
|
|
577
|
+
};
|
|
578
|
+
const layout = compileChart(spec, { width: 320, height: 200 });
|
|
579
|
+
expect(layout.display).toBe('sparkline');
|
|
580
|
+
expect(layout.animation?.enabled).toBe(true);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('breakpoint chrome wins when top-level is sparkline', () => {
|
|
584
|
+
const spec = {
|
|
585
|
+
...lineSpec,
|
|
586
|
+
chrome: undefined,
|
|
587
|
+
display: 'sparkline' as const,
|
|
588
|
+
overrides: { full: { chrome: { title: 'Q4 revenue' } } },
|
|
589
|
+
};
|
|
590
|
+
const layout = compileChart(spec, { width: 1200, height: 600 });
|
|
591
|
+
// At full breakpoint, chrome.title from override should render even
|
|
592
|
+
// though display is sparkline at top-level.
|
|
593
|
+
expect(layout.chrome.title?.text).toBe('Q4 revenue');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('top-level display: sparkline + breakpoint display: full restores all defaults', () => {
|
|
597
|
+
const spec = {
|
|
598
|
+
...lineSpec,
|
|
599
|
+
display: 'sparkline' as const,
|
|
600
|
+
overrides: { full: { display: 'full' as const } },
|
|
601
|
+
};
|
|
602
|
+
const layout = compileChart(spec, { width: 1200, height: 600 });
|
|
603
|
+
expect(layout.display).toBe('full');
|
|
604
|
+
// Watermark default is true in full mode.
|
|
605
|
+
expect(layout.watermark).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('explicit watermark: true in sparkline mode actually paints the watermark', () => {
|
|
609
|
+
const layout = compileChart(
|
|
610
|
+
{
|
|
611
|
+
...lineSpec,
|
|
612
|
+
chrome: undefined,
|
|
613
|
+
display: 'sparkline' as const,
|
|
614
|
+
watermark: true,
|
|
615
|
+
},
|
|
616
|
+
{ width: 400, height: 200 },
|
|
617
|
+
);
|
|
618
|
+
expect(layout.watermark).toBe(true);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('breakpoint encoding.x.axis opts back into x-axis at that breakpoint', () => {
|
|
622
|
+
const spec = {
|
|
623
|
+
...lineSpec,
|
|
624
|
+
chrome: undefined,
|
|
625
|
+
display: 'sparkline' as const,
|
|
626
|
+
overrides: {
|
|
627
|
+
full: {
|
|
628
|
+
encoding: {
|
|
629
|
+
x: { field: 'date', type: 'temporal' as const, axis: { title: 'date' } },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
const layoutFull = compileChart(spec, { width: 1200, height: 600 });
|
|
635
|
+
expect(layoutFull.axes.x).toBeDefined();
|
|
636
|
+
const layoutCompact = compileChart(spec, { width: 320, height: 200 });
|
|
637
|
+
expect(layoutCompact.axes.x).toBeUndefined();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// Animation duration: sparkline mode bumps the entrance to 1100ms when on
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
it('sparkline + animation: true bumps entrance duration to 1100ms', () => {
|
|
645
|
+
const layout = compileChart(
|
|
646
|
+
{ ...lineSpec, display: 'sparkline' as const, animation: true },
|
|
647
|
+
{ width: 400, height: 80 },
|
|
648
|
+
);
|
|
649
|
+
expect(layout.animation?.enabled).toBe(true);
|
|
650
|
+
expect(layout.animation?.duration).toBe(1100);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('sparkline + animation: { enter: { duration: 500 } } respects user duration', () => {
|
|
654
|
+
const layout = compileChart(
|
|
655
|
+
{
|
|
656
|
+
...lineSpec,
|
|
657
|
+
display: 'sparkline' as const,
|
|
658
|
+
animation: { enter: { duration: 500 } },
|
|
659
|
+
},
|
|
660
|
+
{ width: 400, height: 80 },
|
|
661
|
+
);
|
|
662
|
+
expect(layout.animation?.duration).toBe(500);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('full mode + animation: true uses default 500ms (sparkline bump does not leak)', () => {
|
|
666
|
+
const layout = compileChart({ ...lineSpec, animation: true }, { width: 600, height: 400 });
|
|
667
|
+
expect(layout.animation?.duration).toBe(500);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// Breakpoint encoding deep-merge: nested axis state survives an override that
|
|
672
|
+
// only touches one axis property.
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
it('breakpoint encoding.x.axis deep-merges with base axis config', () => {
|
|
676
|
+
const spec = {
|
|
677
|
+
...lineSpec,
|
|
678
|
+
encoding: {
|
|
679
|
+
...lineSpec.encoding,
|
|
680
|
+
x: {
|
|
681
|
+
field: 'date',
|
|
682
|
+
type: 'temporal' as const,
|
|
683
|
+
axis: { title: 'date', tickCount: 8, format: '%b' },
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
overrides: {
|
|
687
|
+
compact: {
|
|
688
|
+
encoding: {
|
|
689
|
+
x: { axis: { title: 'd' } },
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
const layout = compileChart(spec, { width: 320, height: 200 });
|
|
695
|
+
// The compact override only touched axis.title; tickCount and format
|
|
696
|
+
// should survive the deep merge.
|
|
697
|
+
expect(layout.axes.x).toBeDefined();
|
|
698
|
+
expect(layout.axes.x?.label).toBe('d');
|
|
699
|
+
// Tick count flows from base spec — if shallow-merged, tickCount would be lost.
|
|
700
|
+
expect(layout.axes.x?.ticks.length).toBeGreaterThan(0);
|
|
701
|
+
});
|
|
401
702
|
});
|
|
402
703
|
|
|
403
704
|
describe('compileTable', () => {
|