@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.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.
- package/dist/index.d.ts +175 -12
- package/dist/index.js +917 -168
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,1303 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { compileChart, compileGraph, compileTable } from '../compile';
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Test data
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
const lineSpec = {
|
|
9
|
-
mark: 'line' as const,
|
|
10
|
-
data: [
|
|
11
|
-
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
12
|
-
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
13
|
-
{ date: '2020-01-01', value: 15, country: 'UK' },
|
|
14
|
-
{ date: '2021-01-01', value: 35, country: 'UK' },
|
|
15
|
-
],
|
|
16
|
-
encoding: {
|
|
17
|
-
x: { field: 'date', type: 'temporal' as const },
|
|
18
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
19
|
-
color: { field: 'country', type: 'nominal' as const },
|
|
20
|
-
},
|
|
21
|
-
chrome: {
|
|
22
|
-
title: 'GDP Growth',
|
|
23
|
-
subtitle: 'US vs UK over time',
|
|
24
|
-
source: 'World Bank',
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const barSpec = {
|
|
29
|
-
mark: 'bar' as const,
|
|
30
|
-
data: [
|
|
31
|
-
{ name: 'A', value: 10 },
|
|
32
|
-
{ name: 'B', value: 30 },
|
|
33
|
-
{ name: 'C', value: 20 },
|
|
34
|
-
],
|
|
35
|
-
encoding: {
|
|
36
|
-
x: { field: 'value', type: 'quantitative' as const },
|
|
37
|
-
y: { field: 'name', type: 'nominal' as const },
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Tests
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
describe('compileChart', () => {
|
|
46
|
-
it('returns a ChartLayout with all required fields populated', () => {
|
|
47
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
48
|
-
|
|
49
|
-
// Chart area has real dimensions within the total viewport
|
|
50
|
-
expect(layout.area.width).toBeGreaterThan(0);
|
|
51
|
-
expect(layout.area.height).toBeGreaterThan(0);
|
|
52
|
-
expect(layout.area.x).toBeGreaterThanOrEqual(0);
|
|
53
|
-
expect(layout.area.y).toBeGreaterThanOrEqual(0);
|
|
54
|
-
|
|
55
|
-
// Chrome is resolved with positions
|
|
56
|
-
expect(layout.chrome.topHeight).toBeGreaterThan(0);
|
|
57
|
-
expect(layout.chrome.title?.text).toBe('GDP Growth');
|
|
58
|
-
|
|
59
|
-
// Axes have ticks
|
|
60
|
-
expect(layout.axes.x?.ticks.length).toBeGreaterThan(0);
|
|
61
|
-
expect(layout.axes.y?.ticks.length).toBeGreaterThan(0);
|
|
62
|
-
|
|
63
|
-
// Marks were produced by the registered renderer
|
|
64
|
-
expect(layout.marks.length).toBeGreaterThan(0);
|
|
65
|
-
|
|
66
|
-
// Annotations array exists (empty for this spec since none were specified)
|
|
67
|
-
expect(layout.annotations).toEqual([]);
|
|
68
|
-
|
|
69
|
-
// Legend is auto-suppressed for line charts with endpoint labels
|
|
70
|
-
expect(layout.legend.entries.length).toBe(0);
|
|
71
|
-
|
|
72
|
-
// Tooltip descriptors is a Map (may or may not have entries depending on marks)
|
|
73
|
-
expect(layout.tooltipDescriptors).toBeInstanceOf(Map);
|
|
74
|
-
|
|
75
|
-
// Accessibility metadata is populated
|
|
76
|
-
expect(layout.a11y.altText.length).toBeGreaterThan(0);
|
|
77
|
-
expect(layout.a11y.role).toBe('img');
|
|
78
|
-
|
|
79
|
-
// Theme is resolved with actual color values
|
|
80
|
-
expect(layout.theme.colors.background).toBeTruthy();
|
|
81
|
-
expect(layout.theme.colors.categorical.length).toBeGreaterThan(0);
|
|
82
|
-
|
|
83
|
-
// Dimensions match the input
|
|
84
|
-
expect(layout.dimensions).toEqual({ width: 600, height: 400 });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('has correct total dimensions', () => {
|
|
88
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
89
|
-
expect(layout.dimensions.width).toBe(600);
|
|
90
|
-
expect(layout.dimensions.height).toBe(400);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('chart area fits within total dimensions', () => {
|
|
94
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
95
|
-
expect(layout.area.x).toBeGreaterThan(0);
|
|
96
|
-
expect(layout.area.y).toBeGreaterThan(0);
|
|
97
|
-
expect(layout.area.x + layout.area.width).toBeLessThan(600);
|
|
98
|
-
expect(layout.area.y + layout.area.height).toBeLessThan(400);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('chrome title and subtitle are resolved with correct text and positions', () => {
|
|
102
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
103
|
-
expect(layout.chrome.topHeight).toBeGreaterThan(0);
|
|
104
|
-
|
|
105
|
-
// Title
|
|
106
|
-
expect(layout.chrome.title!.text).toBe('GDP Growth');
|
|
107
|
-
expect(layout.chrome.title!.x).toBeGreaterThanOrEqual(0);
|
|
108
|
-
expect(layout.chrome.title!.y).toBeGreaterThan(0);
|
|
109
|
-
expect(layout.chrome.title!.maxWidth).toBeGreaterThan(0);
|
|
110
|
-
expect(layout.chrome.title!.style.fontSize).toBeGreaterThan(0);
|
|
111
|
-
|
|
112
|
-
// Subtitle
|
|
113
|
-
expect(layout.chrome.subtitle!.text).toBe('US vs UK over time');
|
|
114
|
-
expect(layout.chrome.subtitle!.y).toBeGreaterThan(layout.chrome.title!.y);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('bottom chrome source is resolved with text and position', () => {
|
|
118
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
119
|
-
expect(layout.chrome.bottomHeight).toBeGreaterThan(0);
|
|
120
|
-
expect(layout.chrome.source!.text).toBe('World Bank');
|
|
121
|
-
expect(layout.chrome.source!.x).toBeGreaterThanOrEqual(0);
|
|
122
|
-
expect(layout.chrome.source!.y).toBeGreaterThan(0);
|
|
123
|
-
expect(layout.chrome.source!.style.fontSize).toBeGreaterThan(0);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('emits bottomAnchorY below the chart area', () => {
|
|
127
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
128
|
-
expect(layout.chrome.bottomAnchorY).toBeDefined();
|
|
129
|
-
expect(layout.chrome.bottomAnchorY).toBeGreaterThanOrEqual(layout.area.y + layout.area.height);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('has axes with ticks and valid positions for a line chart', () => {
|
|
133
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
134
|
-
|
|
135
|
-
// X axis
|
|
136
|
-
const xTicks = layout.axes.x!.ticks;
|
|
137
|
-
expect(xTicks.length).toBeGreaterThan(0);
|
|
138
|
-
for (const tick of xTicks) {
|
|
139
|
-
expect(tick.position).toBeGreaterThanOrEqual(0);
|
|
140
|
-
expect(tick.label).toBeTruthy();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Y axis
|
|
144
|
-
const yTicks = layout.axes.y!.ticks;
|
|
145
|
-
expect(yTicks.length).toBeGreaterThan(0);
|
|
146
|
-
for (const tick of yTicks) {
|
|
147
|
-
expect(tick.position).toBeGreaterThanOrEqual(0);
|
|
148
|
-
expect(tick.label).toBeTruthy();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Axes have start and end points
|
|
152
|
-
expect(layout.axes.x!.start.x).toBeLessThan(layout.axes.x!.end.x);
|
|
153
|
-
expect(layout.axes.y!.start.y).not.toBe(layout.axes.y!.end.y);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('has a legend when color encoding is present and legend forced on', () => {
|
|
157
|
-
const layout = compileChart(
|
|
158
|
-
{ ...lineSpec, legend: { show: true } },
|
|
159
|
-
{ width: 600, height: 400 },
|
|
160
|
-
);
|
|
161
|
-
expect(layout.legend.entries.length).toBeGreaterThan(0);
|
|
162
|
-
expect(layout.legend.entries.some((e) => e.label === 'US')).toBe(true);
|
|
163
|
-
expect(layout.legend.entries.some((e) => e.label === 'UK')).toBe(true);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('legend entries have colors and shapes', () => {
|
|
167
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
168
|
-
for (const entry of layout.legend.entries) {
|
|
169
|
-
expect(entry.color).toBeTruthy();
|
|
170
|
-
expect(['circle', 'square', 'line']).toContain(entry.shape);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('legend position is "right" at full width', () => {
|
|
175
|
-
const layout = compileChart(lineSpec, { width: 800, height: 400 });
|
|
176
|
-
expect(layout.legend.position).toBe('right');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('legend position is "top" at compact width', () => {
|
|
180
|
-
const layout = compileChart(lineSpec, { width: 320, height: 400 });
|
|
181
|
-
expect(layout.legend.position).toBe('top');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('aligns the top legend left edge to the plot area, not the container edge', () => {
|
|
185
|
-
// A y-axis title + numeric ticks reserves a left gutter, pushing the plot
|
|
186
|
-
// area right of the container padding. The top legend should start at the
|
|
187
|
-
// plot's left edge (above the y-axis guide), not flush against the container.
|
|
188
|
-
const layout = compileChart(
|
|
189
|
-
{
|
|
190
|
-
...lineSpec,
|
|
191
|
-
encoding: {
|
|
192
|
-
...lineSpec.encoding,
|
|
193
|
-
y: { field: 'value', type: 'quantitative' as const, axis: { title: 'GDP ($B)' } },
|
|
194
|
-
},
|
|
195
|
-
legend: { position: 'top' as const, show: true },
|
|
196
|
-
},
|
|
197
|
-
{ width: 360, height: 400 },
|
|
198
|
-
);
|
|
199
|
-
expect(layout.legend.position).toBe('top');
|
|
200
|
-
expect(layout.legend.entries.length).toBeGreaterThan(0);
|
|
201
|
-
// Plot area is inset from the container by the y-axis gutter.
|
|
202
|
-
expect(layout.area.x).toBeGreaterThan(0);
|
|
203
|
-
// Legend left edge now matches the plot left edge (within a pixel).
|
|
204
|
-
expect(layout.legend.bounds.x).toBeCloseTo(layout.area.x, 0);
|
|
205
|
-
// And the legend never paints past the container's right padding edge.
|
|
206
|
-
// Default theme spacing.padding is 20.
|
|
207
|
-
expect(layout.legend.bounds.x + layout.legend.bounds.width).toBeLessThanOrEqual(360 - 20 + 0.5);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('produces line marks with dataPoints (no PointMarks by default)', () => {
|
|
211
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
212
|
-
expect(layout.marks.length).toBeGreaterThan(0);
|
|
213
|
-
|
|
214
|
-
const lineMarks = layout.marks.filter((m) => m.type === 'line');
|
|
215
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
216
|
-
expect(lineMarks.length).toBeGreaterThan(0);
|
|
217
|
-
// Default: no PointMarks (voronoi overlay handles tooltips)
|
|
218
|
-
expect(pointMarks.length).toBe(0);
|
|
219
|
-
|
|
220
|
-
// Line marks should have points and dataPoints with valid coordinates
|
|
221
|
-
for (const mark of lineMarks) {
|
|
222
|
-
if (mark.type === 'line') {
|
|
223
|
-
expect(mark.points.length).toBeGreaterThan(0);
|
|
224
|
-
expect(mark.stroke).toBeTruthy();
|
|
225
|
-
expect(mark.strokeWidth).toBeGreaterThan(0);
|
|
226
|
-
expect(mark.dataPoints).toBeDefined();
|
|
227
|
-
expect(mark.dataPoints!.length).toBeGreaterThan(0);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('produces PointMarks when mark.point is true', () => {
|
|
233
|
-
const specWithPoints = { ...lineSpec, mark: { type: 'line' as const, point: true as const } };
|
|
234
|
-
const layout = compileChart(specWithPoints, { width: 600, height: 400 });
|
|
235
|
-
|
|
236
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
237
|
-
expect(pointMarks.length).toBeGreaterThan(0);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('produces PointMarks on a multi-series area when mark.point is true', () => {
|
|
241
|
-
// Multi-series areas derive their lines from area tops, which bypasses the
|
|
242
|
-
// line renderer's point emission. mark.point must still place dots.
|
|
243
|
-
const areaSpec = {
|
|
244
|
-
...lineSpec,
|
|
245
|
-
mark: { type: 'area' as const, point: true as const },
|
|
246
|
-
};
|
|
247
|
-
const layout = compileChart(areaSpec, { width: 600, height: 400 });
|
|
248
|
-
|
|
249
|
-
const areaMarks = layout.marks.filter((m) => m.type === 'area');
|
|
250
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
251
|
-
expect(areaMarks.length).toBe(2); // US + UK
|
|
252
|
-
// One point per data row across both series (2 points x 2 series).
|
|
253
|
-
expect(pointMarks.length).toBe(4);
|
|
254
|
-
for (const p of pointMarks) {
|
|
255
|
-
if (p.type === 'point') expect(p.r).toBeGreaterThan(0);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('does not produce PointMarks on a multi-series area by default', () => {
|
|
260
|
-
const areaSpec = { ...lineSpec, mark: 'area' as const };
|
|
261
|
-
const layout = compileChart(areaSpec, { width: 600, height: 400 });
|
|
262
|
-
|
|
263
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
264
|
-
expect(pointMarks.length).toBe(0);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('includes accessibility metadata with meaningful content', () => {
|
|
268
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
269
|
-
expect(layout.a11y.altText).toContain('Line chart');
|
|
270
|
-
expect(layout.a11y.altText).toContain('GDP Growth');
|
|
271
|
-
expect(layout.a11y.role).toBe('img');
|
|
272
|
-
expect(layout.a11y.dataTableFallback.length).toBeGreaterThan(0);
|
|
273
|
-
// With marks present, keyboard navigation should be enabled
|
|
274
|
-
expect(layout.a11y.keyboardNavigable).toBe(true);
|
|
275
|
-
// No author opt-out by default
|
|
276
|
-
expect(layout.a11y.hidden).toBeUndefined();
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('a11y.description replaces the auto-generated alt text', () => {
|
|
280
|
-
const spec = { ...lineSpec, a11y: { description: 'GDP growth for US and UK, 2020 to 2021.' } };
|
|
281
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
282
|
-
expect(layout.a11y.altText).toBe('GDP growth for US and UK, 2020 to 2021.');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('top-level description (VL alias) replaces the auto-generated alt text', () => {
|
|
286
|
-
const spec = { ...lineSpec, description: 'Two-country GDP comparison.' };
|
|
287
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
288
|
-
expect(layout.a11y.altText).toBe('Two-country GDP comparison.');
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('a11y.description wins over the top-level description', () => {
|
|
292
|
-
const spec = {
|
|
293
|
-
...lineSpec,
|
|
294
|
-
description: 'Alias text',
|
|
295
|
-
a11y: { description: 'Canonical text' },
|
|
296
|
-
};
|
|
297
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
298
|
-
expect(layout.a11y.altText).toBe('Canonical text');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('a11y.hidden flows into the layout metadata', () => {
|
|
302
|
-
const spec = { ...lineSpec, a11y: { hidden: true } };
|
|
303
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
304
|
-
expect(layout.a11y.hidden).toBe(true);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('produces different strategies at different widths', () => {
|
|
308
|
-
const narrow = compileChart(lineSpec, { width: 320, height: 400 });
|
|
309
|
-
const wide = compileChart(lineSpec, { width: 800, height: 400 });
|
|
310
|
-
|
|
311
|
-
// At 320px the legend should be on top, at 800px on the right
|
|
312
|
-
expect(narrow.legend.position).toBe('top');
|
|
313
|
-
expect(wide.legend.position).toBe('right');
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('dark mode adapts the theme colors', () => {
|
|
317
|
-
const light = compileChart(lineSpec, { width: 600, height: 400 });
|
|
318
|
-
const dark = compileChart(lineSpec, { width: 600, height: 400, darkMode: true });
|
|
319
|
-
|
|
320
|
-
expect(light.theme.isDark).toBe(false);
|
|
321
|
-
expect(dark.theme.isDark).toBe(true);
|
|
322
|
-
// Both modes preserve transparent background — dark mode swaps text/axis/gridline
|
|
323
|
-
// colors but keeps transparency so the host surface shows through.
|
|
324
|
-
expect(dark.theme.colors.background).toBe('transparent');
|
|
325
|
-
expect(light.theme.colors.background).toBe('transparent');
|
|
326
|
-
// Dark mode text should be light, light mode text should be dark
|
|
327
|
-
expect(dark.theme.colors.text).not.toBe(light.theme.colors.text);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('compiles a bar chart with rect marks and both axes', () => {
|
|
331
|
-
const layout = compileChart(barSpec, { width: 600, height: 400 });
|
|
332
|
-
|
|
333
|
-
// Bar charts produce rect marks
|
|
334
|
-
expect(layout.marks.length).toBe(3);
|
|
335
|
-
for (const mark of layout.marks) {
|
|
336
|
-
expect(mark.type).toBe('rect');
|
|
337
|
-
if (mark.type === 'rect') {
|
|
338
|
-
expect(mark.width).toBeGreaterThan(0);
|
|
339
|
-
expect(mark.height).toBeGreaterThan(0);
|
|
340
|
-
expect(mark.fill).toBeTruthy();
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Both axes should be present with ticks
|
|
345
|
-
expect(layout.axes.x!.ticks.length).toBeGreaterThan(0);
|
|
346
|
-
expect(layout.axes.y!.ticks.length).toBe(3); // A, B, C
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('bar chart y-axis labels match the nominal field values', () => {
|
|
350
|
-
const layout = compileChart(barSpec, { width: 600, height: 400 });
|
|
351
|
-
const yLabels = layout.axes.y!.ticks.map((t) => t.label);
|
|
352
|
-
expect(yLabels).toContain('A');
|
|
353
|
-
expect(yLabels).toContain('B');
|
|
354
|
-
expect(yLabels).toContain('C');
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('throws on invalid spec', () => {
|
|
358
|
-
expect(() => compileChart({ type: 'bogus' }, { width: 600, height: 400 })).toThrow();
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('throws if a table spec is passed', () => {
|
|
362
|
-
expect(() =>
|
|
363
|
-
compileChart(
|
|
364
|
-
{ type: 'table', data: [{ a: 1 }], columns: [{ key: 'a' }] },
|
|
365
|
-
{ width: 600, height: 400 },
|
|
366
|
-
),
|
|
367
|
-
).toThrow('compileTable');
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// ---------------------------------------------------------------------------
|
|
371
|
-
// hiddenSeries
|
|
372
|
-
// ---------------------------------------------------------------------------
|
|
373
|
-
|
|
374
|
-
it('hiddenSeries filters out data for hidden series from marks', () => {
|
|
375
|
-
const spec = {
|
|
376
|
-
...lineSpec,
|
|
377
|
-
hiddenSeries: ['UK'],
|
|
378
|
-
legend: { show: true },
|
|
379
|
-
};
|
|
380
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
381
|
-
|
|
382
|
-
// With UK hidden, only US marks should be present.
|
|
383
|
-
// Line marks carry a series property.
|
|
384
|
-
const lineMarks = layout.marks.filter((m) => m.type === 'line');
|
|
385
|
-
for (const mark of lineMarks) {
|
|
386
|
-
if (mark.type === 'line') {
|
|
387
|
-
expect(mark.series).not.toBe('UK');
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Legend should still have entries for both series (hidden ones are dimmed, not removed)
|
|
392
|
-
expect(layout.legend.entries.length).toBe(2);
|
|
393
|
-
expect(layout.legend.entries.some((e) => e.label === 'US')).toBe(true);
|
|
394
|
-
expect(layout.legend.entries.some((e) => e.label === 'UK')).toBe(true);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('hiddenSeries with all series hidden produces no marks', () => {
|
|
398
|
-
const spec = {
|
|
399
|
-
...lineSpec,
|
|
400
|
-
hiddenSeries: ['US', 'UK'],
|
|
401
|
-
};
|
|
402
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
403
|
-
// No data left means no marks
|
|
404
|
-
expect(layout.marks.length).toBe(0);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('hiddenSeries with empty array behaves normally', () => {
|
|
408
|
-
const spec = {
|
|
409
|
-
...lineSpec,
|
|
410
|
-
hiddenSeries: [],
|
|
411
|
-
};
|
|
412
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
413
|
-
expect(layout.marks.length).toBeGreaterThan(0);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('hiddenSeries keeps remaining series on their original palette colors', () => {
|
|
417
|
-
// Regression: filtering renderData by hiddenSeries used to shrink the
|
|
418
|
-
// ordinal color scale's domain, which shifted every visible series down
|
|
419
|
-
// one palette index.
|
|
420
|
-
type LineLike = { type: string; seriesKey?: string; stroke?: string };
|
|
421
|
-
const findStroke = (marks: { type: string }[], series: string) =>
|
|
422
|
-
(marks.find((m) => m.type === 'line' && (m as LineLike).seriesKey === series) as LineLike)
|
|
423
|
-
?.stroke;
|
|
424
|
-
|
|
425
|
-
const baseline = compileChart({ ...lineSpec, hiddenSeries: [] }, { width: 600, height: 400 });
|
|
426
|
-
const baselineUS = findStroke(baseline.marks, 'US');
|
|
427
|
-
const baselineUK = findStroke(baseline.marks, 'UK');
|
|
428
|
-
expect(baselineUS).toBeTruthy();
|
|
429
|
-
expect(baselineUK).toBeTruthy();
|
|
430
|
-
|
|
431
|
-
// Hide UK → US should keep its color.
|
|
432
|
-
const ukHidden = compileChart(
|
|
433
|
-
{ ...lineSpec, hiddenSeries: ['UK'] },
|
|
434
|
-
{ width: 600, height: 400 },
|
|
435
|
-
);
|
|
436
|
-
expect(findStroke(ukHidden.marks, 'US')).toBe(baselineUS);
|
|
437
|
-
|
|
438
|
-
// Hide US → UK should keep its color (reverse direction).
|
|
439
|
-
const usHidden = compileChart(
|
|
440
|
-
{ ...lineSpec, hiddenSeries: ['US'] },
|
|
441
|
-
{ width: 600, height: 400 },
|
|
442
|
-
);
|
|
443
|
-
expect(findStroke(usHidden.marks, 'UK')).toBe(baselineUK);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('hiddenSeries keeps middle-of-N series colors stable in 3-series charts', () => {
|
|
447
|
-
// Pre-fix bug shifted EVERY series after the hidden one — a 2-series
|
|
448
|
-
// case only proves shift-by-one. A 3-series case where the middle is
|
|
449
|
-
// hidden proves the late series doesn't drift either.
|
|
450
|
-
type LineLike = { type: string; seriesKey?: string; stroke?: string };
|
|
451
|
-
const findStroke = (marks: { type: string }[], series: string) =>
|
|
452
|
-
(marks.find((m) => m.type === 'line' && (m as LineLike).seriesKey === series) as LineLike)
|
|
453
|
-
?.stroke;
|
|
454
|
-
|
|
455
|
-
const threeSeriesSpec = {
|
|
456
|
-
mark: 'line' as const,
|
|
457
|
-
data: [
|
|
458
|
-
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
459
|
-
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
460
|
-
{ date: '2020-01-01', value: 15, country: 'UK' },
|
|
461
|
-
{ date: '2021-01-01', value: 35, country: 'UK' },
|
|
462
|
-
{ date: '2020-01-01', value: 12, country: 'JP' },
|
|
463
|
-
{ date: '2021-01-01', value: 38, country: 'JP' },
|
|
464
|
-
],
|
|
465
|
-
encoding: {
|
|
466
|
-
x: { field: 'date', type: 'temporal' as const },
|
|
467
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
468
|
-
color: { field: 'country', type: 'nominal' as const },
|
|
469
|
-
},
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
const baseline = compileChart(threeSeriesSpec, { width: 600, height: 400 });
|
|
473
|
-
const baselineUS = findStroke(baseline.marks, 'US');
|
|
474
|
-
const baselineJP = findStroke(baseline.marks, 'JP');
|
|
475
|
-
|
|
476
|
-
// Hide the middle series.
|
|
477
|
-
const ukHidden = compileChart(
|
|
478
|
-
{ ...threeSeriesSpec, hiddenSeries: ['UK'] },
|
|
479
|
-
{ width: 600, height: 400 },
|
|
480
|
-
);
|
|
481
|
-
expect(findStroke(ukHidden.marks, 'US')).toBe(baselineUS); // first stays
|
|
482
|
-
expect(findStroke(ukHidden.marks, 'JP')).toBe(baselineJP); // last doesn't shift up
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
|
-
// scale.clip
|
|
487
|
-
// ---------------------------------------------------------------------------
|
|
488
|
-
|
|
489
|
-
it('scale.clip filters data rows outside the y-axis domain', () => {
|
|
490
|
-
const spec = {
|
|
491
|
-
mark: 'point' as const,
|
|
492
|
-
data: [
|
|
493
|
-
{ x: 1, y: 5 },
|
|
494
|
-
{ x: 2, y: 15 },
|
|
495
|
-
{ x: 3, y: 25 },
|
|
496
|
-
{ x: 4, y: 35 },
|
|
497
|
-
],
|
|
498
|
-
encoding: {
|
|
499
|
-
x: { field: 'x', type: 'quantitative' as const },
|
|
500
|
-
y: {
|
|
501
|
-
field: 'y',
|
|
502
|
-
type: 'quantitative' as const,
|
|
503
|
-
scale: { domain: [10, 30] as [number, number], clip: true },
|
|
504
|
-
},
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
508
|
-
|
|
509
|
-
// Only y=15 and y=25 should remain (y=5 and y=35 are outside [10,30])
|
|
510
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
511
|
-
expect(pointMarks.length).toBe(2);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('scale.clip filters data rows outside the x-axis domain', () => {
|
|
515
|
-
const spec = {
|
|
516
|
-
mark: 'point' as const,
|
|
517
|
-
data: [
|
|
518
|
-
{ x: 1, y: 10 },
|
|
519
|
-
{ x: 5, y: 20 },
|
|
520
|
-
{ x: 10, y: 30 },
|
|
521
|
-
{ x: 15, y: 40 },
|
|
522
|
-
],
|
|
523
|
-
encoding: {
|
|
524
|
-
x: {
|
|
525
|
-
field: 'x',
|
|
526
|
-
type: 'quantitative' as const,
|
|
527
|
-
scale: { domain: [3, 12] as [number, number], clip: true },
|
|
528
|
-
},
|
|
529
|
-
y: { field: 'y', type: 'quantitative' as const },
|
|
530
|
-
},
|
|
531
|
-
};
|
|
532
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
533
|
-
|
|
534
|
-
// Only x=5 and x=10 should remain
|
|
535
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
536
|
-
expect(pointMarks.length).toBe(2);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it('scale.clip=false does not filter data even with domain set', () => {
|
|
540
|
-
const spec = {
|
|
541
|
-
mark: 'point' as const,
|
|
542
|
-
data: [
|
|
543
|
-
{ x: 1, y: 5 },
|
|
544
|
-
{ x: 2, y: 15 },
|
|
545
|
-
{ x: 3, y: 25 },
|
|
546
|
-
],
|
|
547
|
-
encoding: {
|
|
548
|
-
x: { field: 'x', type: 'quantitative' as const },
|
|
549
|
-
y: {
|
|
550
|
-
field: 'y',
|
|
551
|
-
type: 'quantitative' as const,
|
|
552
|
-
scale: { domain: [10, 20] as [number, number], clip: false },
|
|
553
|
-
},
|
|
554
|
-
},
|
|
555
|
-
};
|
|
556
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
557
|
-
|
|
558
|
-
// All 3 points should still be present (clip is false)
|
|
559
|
-
const pointMarks = layout.marks.filter((m) => m.type === 'point');
|
|
560
|
-
expect(pointMarks.length).toBe(3);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// ---------------------------------------------------------------------------
|
|
564
|
-
// display field + breakpoint override
|
|
565
|
-
// ---------------------------------------------------------------------------
|
|
566
|
-
|
|
567
|
-
it('display defaults to "full" when not specified', () => {
|
|
568
|
-
const layout = compileChart(lineSpec, { width: 600, height: 400 });
|
|
569
|
-
expect(layout.display).toBe('full');
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('display: "sparkline" propagates through to ChartLayout', () => {
|
|
573
|
-
const layout = compileChart(
|
|
574
|
-
{ ...lineSpec, display: 'sparkline' as const },
|
|
575
|
-
{ width: 400, height: 80 },
|
|
576
|
-
);
|
|
577
|
-
expect(layout.display).toBe('sparkline');
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
it('breakpoint override flips display at compact width', () => {
|
|
581
|
-
const spec = {
|
|
582
|
-
...lineSpec,
|
|
583
|
-
overrides: {
|
|
584
|
-
compact: { display: 'sparkline' as const },
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const compactLayout = compileChart(spec, { width: 320, height: 200 });
|
|
589
|
-
expect(compactLayout.display).toBe('sparkline');
|
|
590
|
-
|
|
591
|
-
const desktopLayout = compileChart(spec, { width: 1200, height: 600 });
|
|
592
|
-
expect(desktopLayout.display).toBe('full');
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('sparkline mode forces watermark off when not user-explicit', () => {
|
|
596
|
-
const layout = compileChart(
|
|
597
|
-
{ ...lineSpec, display: 'sparkline' as const },
|
|
598
|
-
{ width: 400, height: 80 },
|
|
599
|
-
);
|
|
600
|
-
expect(layout.watermark).toBe(false);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('sparkline mode respects explicit watermark: true', () => {
|
|
604
|
-
const layout = compileChart(
|
|
605
|
-
{ ...lineSpec, display: 'sparkline' as const, watermark: true },
|
|
606
|
-
{ width: 400, height: 80 },
|
|
607
|
-
);
|
|
608
|
-
expect(layout.watermark).toBe(true);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
it('sparkline mode forces crosshair off when not user-explicit', () => {
|
|
612
|
-
const layout = compileChart(
|
|
613
|
-
{ ...lineSpec, display: 'sparkline' as const },
|
|
614
|
-
{ width: 400, height: 80 },
|
|
615
|
-
);
|
|
616
|
-
expect(layout.crosshair).toBe(false);
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('sparkline mode respects explicit crosshair: true', () => {
|
|
620
|
-
const layout = compileChart(
|
|
621
|
-
{ ...lineSpec, display: 'sparkline' as const, crosshair: true },
|
|
622
|
-
{ width: 400, height: 80 },
|
|
623
|
-
);
|
|
624
|
-
expect(layout.crosshair).toBe(true);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('crosshair defaults on for line marks in full mode', () => {
|
|
628
|
-
const layout = compileChart(lineSpec, { width: 400, height: 300 });
|
|
629
|
-
expect(layout.crosshair).toBe(true);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
it('crosshair respects explicit crosshair: false on a line chart', () => {
|
|
633
|
-
const layout = compileChart({ ...lineSpec, crosshair: false }, { width: 400, height: 300 });
|
|
634
|
-
expect(layout.crosshair).toBe(false);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
// ---------------------------------------------------------------------------
|
|
638
|
-
// Sparkline layout profile (dimensions, axes, legend)
|
|
639
|
-
// ---------------------------------------------------------------------------
|
|
640
|
-
|
|
641
|
-
it('sparkline produces near-edge-to-edge chart area (margins <= 4px per side)', () => {
|
|
642
|
-
const layout = compileChart(
|
|
643
|
-
{ ...lineSpec, chrome: undefined, display: 'sparkline' as const },
|
|
644
|
-
{ width: 400, height: 80 },
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
// No chrome, no axes, no legend by default. Mark area should be tight.
|
|
648
|
-
expect(layout.area.x).toBeLessThanOrEqual(4);
|
|
649
|
-
expect(layout.area.y).toBeLessThanOrEqual(4);
|
|
650
|
-
const rightMargin = 400 - (layout.area.x + layout.area.width);
|
|
651
|
-
const bottomMargin = 80 - (layout.area.y + layout.area.height);
|
|
652
|
-
expect(rightMargin).toBeLessThanOrEqual(4);
|
|
653
|
-
expect(bottomMargin).toBeLessThanOrEqual(4);
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
it('sparkline returns no axes by default', () => {
|
|
657
|
-
const layout = compileChart(
|
|
658
|
-
{ ...lineSpec, chrome: undefined, display: 'sparkline' as const },
|
|
659
|
-
{ width: 400, height: 80 },
|
|
660
|
-
);
|
|
661
|
-
expect(layout.axes.x).toBeUndefined();
|
|
662
|
-
expect(layout.axes.y).toBeUndefined();
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it('sparkline + explicit encoding.x.axis still reserves x-axis', () => {
|
|
666
|
-
const layout = compileChart(
|
|
667
|
-
{
|
|
668
|
-
...lineSpec,
|
|
669
|
-
chrome: undefined,
|
|
670
|
-
display: 'sparkline' as const,
|
|
671
|
-
encoding: {
|
|
672
|
-
...lineSpec.encoding,
|
|
673
|
-
x: { ...lineSpec.encoding.x, axis: { title: 'date' } },
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
{ width: 400, height: 200 },
|
|
677
|
-
);
|
|
678
|
-
expect(layout.axes.x).toBeDefined();
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it('sparkline + explicit chrome.title still renders chrome', () => {
|
|
682
|
-
const layout = compileChart(
|
|
683
|
-
{
|
|
684
|
-
...lineSpec,
|
|
685
|
-
chrome: { title: 'Q4 Revenue' },
|
|
686
|
-
display: 'sparkline' as const,
|
|
687
|
-
},
|
|
688
|
-
{ width: 400, height: 200 },
|
|
689
|
-
);
|
|
690
|
-
expect(layout.chrome.title).toBeDefined();
|
|
691
|
-
expect(layout.chrome.title?.text).toBe('Q4 Revenue');
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
it('sparkline hides legend by default even with color encoding', () => {
|
|
695
|
-
const layout = compileChart(
|
|
696
|
-
{
|
|
697
|
-
...lineSpec,
|
|
698
|
-
chrome: undefined,
|
|
699
|
-
display: 'sparkline' as const,
|
|
700
|
-
},
|
|
701
|
-
{ width: 400, height: 80 },
|
|
702
|
-
);
|
|
703
|
-
expect('entries' in layout.legend && layout.legend.entries.length).toBe(0);
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it('sparkline + explicit legend.show: true renders legend', () => {
|
|
707
|
-
const layout = compileChart(
|
|
708
|
-
{
|
|
709
|
-
...lineSpec,
|
|
710
|
-
chrome: undefined,
|
|
711
|
-
display: 'sparkline' as const,
|
|
712
|
-
legend: { show: true },
|
|
713
|
-
},
|
|
714
|
-
{ width: 400, height: 200 },
|
|
715
|
-
);
|
|
716
|
-
expect('entries' in layout.legend && layout.legend.entries.length).toBeGreaterThan(0);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it('sparkline works at heights as low as 30px', () => {
|
|
720
|
-
const layout = compileChart(
|
|
721
|
-
{ ...lineSpec, chrome: undefined, display: 'sparkline' as const },
|
|
722
|
-
{ width: 200, height: 30 },
|
|
723
|
-
);
|
|
724
|
-
expect(layout.area.height).toBeGreaterThan(0);
|
|
725
|
-
expect(layout.area.width).toBeGreaterThan(0);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
it('chrome: {} does not count as user-explicit chrome (still stripped in sparkline)', () => {
|
|
729
|
-
// Empty chrome object is the idiom for "silence defaults" — should not
|
|
730
|
-
// opt-in to chrome rendering in sparkline mode.
|
|
731
|
-
const layout = compileChart(
|
|
732
|
-
{ ...lineSpec, chrome: {}, display: 'sparkline' as const },
|
|
733
|
-
{ width: 400, height: 80 },
|
|
734
|
-
);
|
|
735
|
-
expect(layout.chrome.title).toBeUndefined();
|
|
736
|
-
expect(layout.chrome.topHeight).toBe(0);
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// -------------------------------------------------------------------------
|
|
740
|
-
// Sparkline visual defaults (trend color, endpoint dot, gradient, bar pill)
|
|
741
|
-
// -------------------------------------------------------------------------
|
|
742
|
-
|
|
743
|
-
const upTrendData = [
|
|
744
|
-
{ date: '2026-01-01', value: 10 },
|
|
745
|
-
{ date: '2026-01-02', value: 12 },
|
|
746
|
-
{ date: '2026-01-03', value: 14 },
|
|
747
|
-
{ date: '2026-01-04', value: 18 },
|
|
748
|
-
{ date: '2026-01-05', value: 22 },
|
|
749
|
-
];
|
|
750
|
-
|
|
751
|
-
const downTrendData = [
|
|
752
|
-
{ date: '2026-01-01', value: 22 },
|
|
753
|
-
{ date: '2026-01-02', value: 18 },
|
|
754
|
-
{ date: '2026-01-03', value: 14 },
|
|
755
|
-
{ date: '2026-01-04', value: 12 },
|
|
756
|
-
{ date: '2026-01-05', value: 10 },
|
|
757
|
-
];
|
|
758
|
-
|
|
759
|
-
const sparkLineSpec = {
|
|
760
|
-
mark: 'line' as const,
|
|
761
|
-
data: upTrendData,
|
|
762
|
-
encoding: {
|
|
763
|
-
x: { field: 'date', type: 'temporal' as const },
|
|
764
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
765
|
-
},
|
|
766
|
-
display: 'sparkline' as const,
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
it('sparkline line emits a single decorative endpoint dot at the last point', () => {
|
|
770
|
-
const layout = compileChart(sparkLineSpec, { width: 200, height: 40 });
|
|
771
|
-
const points = layout.marks.filter((m) => m.type === 'point');
|
|
772
|
-
expect(points.length).toBe(1);
|
|
773
|
-
expect(points[0].aria?.decorative).toBe(true);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it('sparkline line uses the positive theme token for an up-trending series', () => {
|
|
777
|
-
const layout = compileChart(sparkLineSpec, { width: 200, height: 40 });
|
|
778
|
-
const lineMark = layout.marks.find((m) => m.type === 'line');
|
|
779
|
-
expect(lineMark?.stroke).toBe('#16a34a');
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it('sparkline line uses the negative theme token for a down-trending series', () => {
|
|
783
|
-
const layout = compileChart(
|
|
784
|
-
{ ...sparkLineSpec, data: downTrendData },
|
|
785
|
-
{ width: 200, height: 40 },
|
|
786
|
-
);
|
|
787
|
-
const lineMark = layout.marks.find((m) => m.type === 'line');
|
|
788
|
-
expect(lineMark?.stroke).toBe('#dc2626');
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it('explicit markDef.stroke wins over the trend default in sparkline mode', () => {
|
|
792
|
-
const layout = compileChart(
|
|
793
|
-
{ ...sparkLineSpec, mark: { type: 'line' as const, stroke: '#ff00ff' } },
|
|
794
|
-
{ width: 200, height: 40 },
|
|
795
|
-
);
|
|
796
|
-
const lineMark = layout.marks.find((m) => m.type === 'line');
|
|
797
|
-
expect(lineMark?.stroke).toBe('#ff00ff');
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
it('explicit markDef.point: false suppresses the auto-injected endpoint dot', () => {
|
|
801
|
-
const layout = compileChart(
|
|
802
|
-
{ ...sparkLineSpec, mark: { type: 'line' as const, point: false } },
|
|
803
|
-
{ width: 200, height: 40 },
|
|
804
|
-
);
|
|
805
|
-
const points = layout.marks.filter((m) => m.type === 'point');
|
|
806
|
-
expect(points.length).toBe(0);
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
it('sparkline area injects a trend-colored gradient fill by default', () => {
|
|
810
|
-
const layout = compileChart(
|
|
811
|
-
{ ...sparkLineSpec, mark: 'area' as const },
|
|
812
|
-
{ width: 200, height: 40 },
|
|
813
|
-
);
|
|
814
|
-
const areaMark = layout.marks.find((m) => m.type === 'area');
|
|
815
|
-
// fill should be a gradient object (not a flat string)
|
|
816
|
-
expect(typeof areaMark?.fill).toBe('object');
|
|
817
|
-
expect((areaMark?.fill as { gradient?: string }).gradient).toBe('linear');
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
it('sparkline single-series vertical bars use a [min, max] domain', () => {
|
|
821
|
-
// With values 100, 105, 110, 120 and zero: false, the y-domain should
|
|
822
|
-
// start near 100 — not 0 — so the shortest bar is still visible.
|
|
823
|
-
const layout = compileChart(
|
|
824
|
-
{
|
|
825
|
-
mark: 'bar' as const,
|
|
826
|
-
data: [
|
|
827
|
-
{ cat: 'a', value: 100 },
|
|
828
|
-
{ cat: 'b', value: 105 },
|
|
829
|
-
{ cat: 'c', value: 110 },
|
|
830
|
-
{ cat: 'd', value: 120 },
|
|
831
|
-
],
|
|
832
|
-
encoding: {
|
|
833
|
-
x: { field: 'cat', type: 'nominal' as const },
|
|
834
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
835
|
-
},
|
|
836
|
-
display: 'sparkline' as const,
|
|
837
|
-
},
|
|
838
|
-
{ width: 200, height: 40 },
|
|
839
|
-
);
|
|
840
|
-
// The shortest bar (value 100) should NOT have a height that fills the
|
|
841
|
-
// chart down from y=0; with zero:false it sits near the bottom.
|
|
842
|
-
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
843
|
-
expect(rects.length).toBe(4);
|
|
844
|
-
// Heights should differ meaningfully across the four bars.
|
|
845
|
-
const heights = rects.map((r) => (r as { height: number }).height);
|
|
846
|
-
const maxH = Math.max(...heights);
|
|
847
|
-
const minH = Math.min(...heights);
|
|
848
|
-
expect(maxH - minH).toBeGreaterThan(maxH * 0.5);
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
it('sparkline stacked vertical bars retain the [0, max] baseline', () => {
|
|
852
|
-
// Stacked bars MUST baseline at zero — non-zero baselines break stack
|
|
853
|
-
// arithmetic. The total bar should equal the category sum.
|
|
854
|
-
const layout = compileChart(
|
|
855
|
-
{
|
|
856
|
-
mark: 'bar' as const,
|
|
857
|
-
data: [
|
|
858
|
-
{ cat: 'a', series: 's1', value: 50 },
|
|
859
|
-
{ cat: 'a', series: 's2', value: 50 },
|
|
860
|
-
{ cat: 'b', series: 's1', value: 30 },
|
|
861
|
-
{ cat: 'b', series: 's2', value: 70 },
|
|
862
|
-
],
|
|
863
|
-
encoding: {
|
|
864
|
-
x: { field: 'cat', type: 'nominal' as const },
|
|
865
|
-
y: { field: 'value', type: 'quantitative' as const, stack: 'zero' as const },
|
|
866
|
-
color: { field: 'series', type: 'nominal' as const },
|
|
867
|
-
},
|
|
868
|
-
display: 'sparkline' as const,
|
|
869
|
-
},
|
|
870
|
-
{ width: 200, height: 80 },
|
|
871
|
-
);
|
|
872
|
-
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
873
|
-
expect(rects.length).toBe(4);
|
|
874
|
-
// For stacked bars, both categories sum to the same total (100), so the
|
|
875
|
-
// tallest stacked column should fill close to the full chart area height.
|
|
876
|
-
// Verify by checking that at least one bar starts at the chart-area top
|
|
877
|
-
// (its y matches chartArea.y within a rounding tolerance).
|
|
878
|
-
const minY = Math.min(...rects.map((r) => (r as { y: number }).y));
|
|
879
|
-
expect(minY).toBeCloseTo(layout.area.y, -1);
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
it('sparkline bar gets pill cornerRadius by default', () => {
|
|
883
|
-
const layout = compileChart(
|
|
884
|
-
{
|
|
885
|
-
mark: 'bar' as const,
|
|
886
|
-
data: [
|
|
887
|
-
{ cat: 'a', value: 5 },
|
|
888
|
-
{ cat: 'b', value: 8 },
|
|
889
|
-
{ cat: 'c', value: 6 },
|
|
890
|
-
],
|
|
891
|
-
encoding: {
|
|
892
|
-
x: { field: 'cat', type: 'nominal' as const },
|
|
893
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
894
|
-
},
|
|
895
|
-
display: 'sparkline' as const,
|
|
896
|
-
},
|
|
897
|
-
{ width: 200, height: 40 },
|
|
898
|
-
);
|
|
899
|
-
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
900
|
-
expect(rects.length).toBeGreaterThan(0);
|
|
901
|
-
// 'pill' translates to rx = barWidth / 2 in column compute. Each rect's
|
|
902
|
-
// cornerRadius should equal half the bar's width (or thickness).
|
|
903
|
-
const r = rects[0] as { width: number; cornerRadius: number };
|
|
904
|
-
expect(r.cornerRadius).toBeCloseTo(r.width / 2, 1);
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
it('sparkline stacked bars: only top segment per stack gets pill rounding (top corners)', () => {
|
|
908
|
-
// Stacked segments below the top stay square so the seams between
|
|
909
|
-
// segments are flush. Each topmost segment receives the pill radius
|
|
910
|
-
// with `cornerRadiusSides` constrained to the top corners — the
|
|
911
|
-
// renderer emits a path with rounded top + square bottom.
|
|
912
|
-
const layout = compileChart(
|
|
913
|
-
{
|
|
914
|
-
mark: 'bar' as const,
|
|
915
|
-
data: [
|
|
916
|
-
{ cat: 'a', series: 's1', value: 50 },
|
|
917
|
-
{ cat: 'a', series: 's2', value: 50 },
|
|
918
|
-
{ cat: 'b', series: 's1', value: 30 },
|
|
919
|
-
{ cat: 'b', series: 's2', value: 70 },
|
|
920
|
-
],
|
|
921
|
-
encoding: {
|
|
922
|
-
x: { field: 'cat', type: 'nominal' as const },
|
|
923
|
-
y: { field: 'value', type: 'quantitative' as const, stack: 'zero' as const },
|
|
924
|
-
color: { field: 'series', type: 'nominal' as const },
|
|
925
|
-
},
|
|
926
|
-
display: 'sparkline' as const,
|
|
927
|
-
},
|
|
928
|
-
{ width: 200, height: 80 },
|
|
929
|
-
);
|
|
930
|
-
type Rect = {
|
|
931
|
-
type: 'rect';
|
|
932
|
-
cornerRadius?: number;
|
|
933
|
-
cornerRadiusSides?: { tl?: boolean; tr?: boolean; br?: boolean; bl?: boolean };
|
|
934
|
-
stackGroup?: string;
|
|
935
|
-
y: number;
|
|
936
|
-
};
|
|
937
|
-
const rects = layout.marks.filter((m): m is Rect => m.type === 'rect') as Rect[];
|
|
938
|
-
|
|
939
|
-
// Group by stackGroup, find topmost (smallest y) per group.
|
|
940
|
-
const tops = new Map<string, Rect>();
|
|
941
|
-
for (const r of rects) {
|
|
942
|
-
if (!r.stackGroup) continue;
|
|
943
|
-
const cur = tops.get(r.stackGroup);
|
|
944
|
-
if (!cur || r.y < cur.y) tops.set(r.stackGroup, r);
|
|
945
|
-
}
|
|
946
|
-
expect(tops.size).toBe(2);
|
|
947
|
-
|
|
948
|
-
for (const r of rects) {
|
|
949
|
-
const isTop = tops.get(r.stackGroup ?? '') === r;
|
|
950
|
-
if (isTop) {
|
|
951
|
-
expect(r.cornerRadius).toBeGreaterThan(0);
|
|
952
|
-
expect(r.cornerRadiusSides).toEqual({
|
|
953
|
-
tl: true,
|
|
954
|
-
tr: true,
|
|
955
|
-
br: false,
|
|
956
|
-
bl: false,
|
|
957
|
-
});
|
|
958
|
-
} else {
|
|
959
|
-
expect(r.cornerRadius ?? 0).toBe(0);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
// ---------------------------------------------------------------------------
|
|
965
|
-
// Explicit-at-any-level wins (precedence matrix)
|
|
966
|
-
// ---------------------------------------------------------------------------
|
|
967
|
-
|
|
968
|
-
it('top-level animation: true wins even when breakpoint flips to sparkline', () => {
|
|
969
|
-
const spec = {
|
|
970
|
-
...lineSpec,
|
|
971
|
-
animation: true as const,
|
|
972
|
-
overrides: { compact: { display: 'sparkline' as const } },
|
|
973
|
-
};
|
|
974
|
-
const layout = compileChart(spec, { width: 320, height: 200 });
|
|
975
|
-
expect(layout.display).toBe('sparkline');
|
|
976
|
-
expect(layout.animation?.enter).toBeDefined();
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
it('breakpoint chrome wins when top-level is sparkline', () => {
|
|
980
|
-
const spec = {
|
|
981
|
-
...lineSpec,
|
|
982
|
-
chrome: undefined,
|
|
983
|
-
display: 'sparkline' as const,
|
|
984
|
-
overrides: { full: { chrome: { title: 'Q4 revenue' } } },
|
|
985
|
-
};
|
|
986
|
-
const layout = compileChart(spec, { width: 1200, height: 600 });
|
|
987
|
-
// At full breakpoint, chrome.title from override should render even
|
|
988
|
-
// though display is sparkline at top-level.
|
|
989
|
-
expect(layout.chrome.title?.text).toBe('Q4 revenue');
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
it('top-level display: sparkline + breakpoint display: full restores all defaults', () => {
|
|
993
|
-
const spec = {
|
|
994
|
-
...lineSpec,
|
|
995
|
-
display: 'sparkline' as const,
|
|
996
|
-
overrides: { full: { display: 'full' as const } },
|
|
997
|
-
};
|
|
998
|
-
const layout = compileChart(spec, { width: 1200, height: 600 });
|
|
999
|
-
expect(layout.display).toBe('full');
|
|
1000
|
-
// Watermark default is true in full mode.
|
|
1001
|
-
expect(layout.watermark).toBe(true);
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
it('explicit watermark: true in sparkline mode actually paints the watermark', () => {
|
|
1005
|
-
const layout = compileChart(
|
|
1006
|
-
{
|
|
1007
|
-
...lineSpec,
|
|
1008
|
-
chrome: undefined,
|
|
1009
|
-
display: 'sparkline' as const,
|
|
1010
|
-
watermark: true,
|
|
1011
|
-
},
|
|
1012
|
-
{ width: 400, height: 200 },
|
|
1013
|
-
);
|
|
1014
|
-
expect(layout.watermark).toBe(true);
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
it('breakpoint encoding.x.axis opts back into x-axis at that breakpoint', () => {
|
|
1018
|
-
const spec = {
|
|
1019
|
-
...lineSpec,
|
|
1020
|
-
chrome: undefined,
|
|
1021
|
-
display: 'sparkline' as const,
|
|
1022
|
-
overrides: {
|
|
1023
|
-
full: {
|
|
1024
|
-
encoding: {
|
|
1025
|
-
x: { field: 'date', type: 'temporal' as const, axis: { title: 'date' } },
|
|
1026
|
-
},
|
|
1027
|
-
},
|
|
1028
|
-
},
|
|
1029
|
-
};
|
|
1030
|
-
const layoutFull = compileChart(spec, { width: 1200, height: 600 });
|
|
1031
|
-
expect(layoutFull.axes.x).toBeDefined();
|
|
1032
|
-
const layoutCompact = compileChart(spec, { width: 320, height: 200 });
|
|
1033
|
-
expect(layoutCompact.axes.x).toBeUndefined();
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
// ---------------------------------------------------------------------------
|
|
1037
|
-
// Animation duration: sparkline mode bumps the entrance to 1100ms when on
|
|
1038
|
-
// ---------------------------------------------------------------------------
|
|
1039
|
-
|
|
1040
|
-
it('sparkline + animation: true bumps entrance duration to 1100ms', () => {
|
|
1041
|
-
const layout = compileChart(
|
|
1042
|
-
{ ...lineSpec, display: 'sparkline' as const, animation: true },
|
|
1043
|
-
{ width: 400, height: 80 },
|
|
1044
|
-
);
|
|
1045
|
-
expect(layout.animation?.enter).toBeDefined();
|
|
1046
|
-
expect(layout.animation?.enter?.duration).toBe(1100);
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
it('sparkline + animation: { enter: { duration: 500 } } respects user duration', () => {
|
|
1050
|
-
const layout = compileChart(
|
|
1051
|
-
{
|
|
1052
|
-
...lineSpec,
|
|
1053
|
-
display: 'sparkline' as const,
|
|
1054
|
-
animation: { enter: { duration: 500 } },
|
|
1055
|
-
},
|
|
1056
|
-
{ width: 400, height: 80 },
|
|
1057
|
-
);
|
|
1058
|
-
expect(layout.animation?.enter?.duration).toBe(500);
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
it('full mode + animation: true uses default 500ms (sparkline bump does not leak)', () => {
|
|
1062
|
-
const layout = compileChart({ ...lineSpec, animation: true }, { width: 600, height: 400 });
|
|
1063
|
-
expect(layout.animation?.enter?.duration).toBe(500);
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
// ---------------------------------------------------------------------------
|
|
1067
|
-
// Breakpoint encoding deep-merge: nested axis state survives an override that
|
|
1068
|
-
// only touches one axis property.
|
|
1069
|
-
// ---------------------------------------------------------------------------
|
|
1070
|
-
|
|
1071
|
-
it('breakpoint encoding.x.axis deep-merges with base axis config', () => {
|
|
1072
|
-
const spec = {
|
|
1073
|
-
...lineSpec,
|
|
1074
|
-
encoding: {
|
|
1075
|
-
...lineSpec.encoding,
|
|
1076
|
-
x: {
|
|
1077
|
-
field: 'date',
|
|
1078
|
-
type: 'temporal' as const,
|
|
1079
|
-
axis: { title: 'date', tickCount: 8, format: '%b' },
|
|
1080
|
-
},
|
|
1081
|
-
},
|
|
1082
|
-
overrides: {
|
|
1083
|
-
compact: {
|
|
1084
|
-
encoding: {
|
|
1085
|
-
x: { axis: { title: 'd' } },
|
|
1086
|
-
},
|
|
1087
|
-
},
|
|
1088
|
-
},
|
|
1089
|
-
};
|
|
1090
|
-
const layout = compileChart(spec, { width: 320, height: 200 });
|
|
1091
|
-
// The compact override only touched axis.title; tickCount and format
|
|
1092
|
-
// should survive the deep merge.
|
|
1093
|
-
expect(layout.axes.x).toBeDefined();
|
|
1094
|
-
expect(layout.axes.x?.label).toBe('d');
|
|
1095
|
-
// Tick count flows from base spec — if shallow-merged, tickCount would be lost.
|
|
1096
|
-
expect(layout.axes.x?.ticks.length).toBeGreaterThan(0);
|
|
1097
|
-
});
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
describe('compileTable', () => {
|
|
1101
|
-
it('returns a valid TableLayout with resolved columns and rows', () => {
|
|
1102
|
-
const layout = compileTable(
|
|
1103
|
-
{
|
|
1104
|
-
type: 'table',
|
|
1105
|
-
data: [{ name: 'Alice', age: 30 }],
|
|
1106
|
-
columns: [{ key: 'name' }, { key: 'age' }],
|
|
1107
|
-
chrome: { title: 'People' },
|
|
1108
|
-
},
|
|
1109
|
-
{ width: 600, height: 400 },
|
|
1110
|
-
);
|
|
1111
|
-
|
|
1112
|
-
// Chrome title is resolved
|
|
1113
|
-
expect(layout.chrome.title!.text).toBe('People');
|
|
1114
|
-
expect(layout.chrome.topHeight).toBeGreaterThan(0);
|
|
1115
|
-
|
|
1116
|
-
// Columns are resolved with labels and widths
|
|
1117
|
-
expect(layout.columns).toHaveLength(2);
|
|
1118
|
-
expect(layout.columns[0].key).toBe('name');
|
|
1119
|
-
expect(layout.columns[0].label).toBeTruthy();
|
|
1120
|
-
expect(layout.columns[0].width).toBeGreaterThan(0);
|
|
1121
|
-
expect(layout.columns[1].key).toBe('age');
|
|
1122
|
-
|
|
1123
|
-
// Rows contain formatted cell values
|
|
1124
|
-
expect(layout.rows).toHaveLength(1);
|
|
1125
|
-
expect(layout.rows[0].cells).toHaveLength(2);
|
|
1126
|
-
expect(layout.rows[0].cells[0].formattedValue).toBe('Alice');
|
|
1127
|
-
expect(layout.rows[0].cells[1].formattedValue).toBe('30');
|
|
1128
|
-
|
|
1129
|
-
// Accessibility caption includes the title
|
|
1130
|
-
expect(layout.a11y.caption).toContain('People');
|
|
1131
|
-
|
|
1132
|
-
// Theme is resolved
|
|
1133
|
-
expect(layout.theme.isDark).toBe(false);
|
|
1134
|
-
expect(layout.theme.colors.background).toBeTruthy();
|
|
1135
|
-
});
|
|
1136
|
-
|
|
1137
|
-
it('throws if a chart spec is passed', () => {
|
|
1138
|
-
expect(() => compileTable(lineSpec, { width: 600, height: 400 })).toThrow('compileChart');
|
|
1139
|
-
});
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
describe('compileGraph', () => {
|
|
1143
|
-
it('returns a valid GraphCompilation for a graph spec', () => {
|
|
1144
|
-
const result = compileGraph(
|
|
1145
|
-
{
|
|
1146
|
-
type: 'graph',
|
|
1147
|
-
nodes: [{ id: 'a' }],
|
|
1148
|
-
edges: [],
|
|
1149
|
-
},
|
|
1150
|
-
{ width: 600, height: 400 },
|
|
1151
|
-
);
|
|
1152
|
-
expect(result.nodes).toHaveLength(1);
|
|
1153
|
-
expect(result.edges).toHaveLength(0);
|
|
1154
|
-
expect(result.dimensions).toEqual({ width: 600, height: 400 });
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
it('throws for non-graph specs', () => {
|
|
1158
|
-
expect(() =>
|
|
1159
|
-
compileGraph(
|
|
1160
|
-
{
|
|
1161
|
-
mark: 'point',
|
|
1162
|
-
data: [{ x: 1, y: 2 }],
|
|
1163
|
-
encoding: {
|
|
1164
|
-
x: { field: 'x', type: 'quantitative' },
|
|
1165
|
-
y: { field: 'y', type: 'quantitative' },
|
|
1166
|
-
},
|
|
1167
|
-
},
|
|
1168
|
-
{ width: 600, height: 400 },
|
|
1169
|
-
),
|
|
1170
|
-
).toThrow('compileGraph received a non-graph spec');
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
it('propagates labelAngle through the full compilation pipeline', () => {
|
|
1174
|
-
const columnSpec = {
|
|
1175
|
-
mark: 'bar' as const,
|
|
1176
|
-
data: [
|
|
1177
|
-
{ state: 'California', pop: 39000000 },
|
|
1178
|
-
{ state: 'Texas', pop: 29000000 },
|
|
1179
|
-
{ state: 'Florida', pop: 22000000 },
|
|
1180
|
-
{ state: 'New York', pop: 20000000 },
|
|
1181
|
-
{ state: 'Pennsylvania', pop: 13000000 },
|
|
1182
|
-
],
|
|
1183
|
-
encoding: {
|
|
1184
|
-
x: { field: 'state', type: 'nominal' as const, axis: { labelAngle: -90 } },
|
|
1185
|
-
y: { field: 'pop', type: 'quantitative' as const },
|
|
1186
|
-
},
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
const layout = compileChart(columnSpec, { width: 400, height: 300 });
|
|
1190
|
-
|
|
1191
|
-
// labelAngle should be propagated to the x-axis layout as tickAngle
|
|
1192
|
-
expect(layout.axes.x!.tickAngle).toBe(-90);
|
|
1193
|
-
// y-axis should not have a tickAngle
|
|
1194
|
-
expect(layout.axes.y!.tickAngle).toBeUndefined();
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
it('reserves extra bottom margin for rotated x-axis labels', () => {
|
|
1198
|
-
const baseColumnSpec = {
|
|
1199
|
-
mark: 'bar' as const,
|
|
1200
|
-
data: [
|
|
1201
|
-
{ state: 'California', pop: 39000000 },
|
|
1202
|
-
{ state: 'Texas', pop: 29000000 },
|
|
1203
|
-
{ state: 'Massachusetts', pop: 7000000 },
|
|
1204
|
-
],
|
|
1205
|
-
encoding: {
|
|
1206
|
-
x: { field: 'state', type: 'nominal' as const },
|
|
1207
|
-
y: { field: 'pop', type: 'quantitative' as const },
|
|
1208
|
-
},
|
|
1209
|
-
};
|
|
1210
|
-
const rotatedColumnSpec = {
|
|
1211
|
-
...baseColumnSpec,
|
|
1212
|
-
encoding: {
|
|
1213
|
-
x: { field: 'state', type: 'nominal' as const, axis: { labelAngle: -90 } },
|
|
1214
|
-
y: { field: 'pop', type: 'quantitative' as const },
|
|
1215
|
-
},
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
const layoutNormal = compileChart(baseColumnSpec, { width: 400, height: 300 });
|
|
1219
|
-
const layoutRotated = compileChart(rotatedColumnSpec, { width: 400, height: 300 });
|
|
1220
|
-
|
|
1221
|
-
// Rotated labels should shrink the chart area height
|
|
1222
|
-
expect(layoutRotated.area.height).toBeLessThan(layoutNormal.area.height);
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
it('hides legend when legend.show is false', () => {
|
|
1226
|
-
const spec = {
|
|
1227
|
-
...lineSpec,
|
|
1228
|
-
legend: { show: false },
|
|
1229
|
-
};
|
|
1230
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
1231
|
-
expect(layout.legend.entries).toHaveLength(0);
|
|
1232
|
-
expect(layout.legend.bounds.width).toBe(0);
|
|
1233
|
-
expect(layout.legend.bounds.height).toBe(0);
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
it('applies compact breakpoint overrides to chrome', () => {
|
|
1237
|
-
const spec = {
|
|
1238
|
-
...lineSpec,
|
|
1239
|
-
chrome: {
|
|
1240
|
-
title: 'Full width title with extra context',
|
|
1241
|
-
subtitle: 'Long subtitle with methodology details',
|
|
1242
|
-
},
|
|
1243
|
-
overrides: {
|
|
1244
|
-
compact: {
|
|
1245
|
-
chrome: {
|
|
1246
|
-
title: 'Short title',
|
|
1247
|
-
subtitle: 'Short sub',
|
|
1248
|
-
},
|
|
1249
|
-
},
|
|
1250
|
-
},
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
// At compact width (< 400), overrides should apply
|
|
1254
|
-
const compactLayout = compileChart(spec, { width: 350, height: 400 });
|
|
1255
|
-
expect(compactLayout.chrome.title!.text).toBe('Short title');
|
|
1256
|
-
expect(compactLayout.chrome.subtitle!.text).toBe('Short sub');
|
|
1257
|
-
|
|
1258
|
-
// At full width, base spec should apply
|
|
1259
|
-
const fullLayout = compileChart(spec, { width: 800, height: 400 });
|
|
1260
|
-
expect(fullLayout.chrome.title!.text).toBe('Full width title with extra context');
|
|
1261
|
-
expect(fullLayout.chrome.subtitle!.text).toBe('Long subtitle with methodology details');
|
|
1262
|
-
});
|
|
1263
|
-
|
|
1264
|
-
it('applies medium breakpoint overrides', () => {
|
|
1265
|
-
const spec = {
|
|
1266
|
-
...lineSpec,
|
|
1267
|
-
chrome: { title: 'Full title' },
|
|
1268
|
-
overrides: {
|
|
1269
|
-
medium: {
|
|
1270
|
-
chrome: { title: 'Medium title' },
|
|
1271
|
-
},
|
|
1272
|
-
},
|
|
1273
|
-
};
|
|
1274
|
-
|
|
1275
|
-
// At medium width (400-700), override should apply
|
|
1276
|
-
const mediumLayout = compileChart(spec, { width: 500, height: 400 });
|
|
1277
|
-
expect(mediumLayout.chrome.title!.text).toBe('Medium title');
|
|
1278
|
-
|
|
1279
|
-
// At full width, base spec should apply
|
|
1280
|
-
const fullLayout = compileChart(spec, { width: 800, height: 400 });
|
|
1281
|
-
expect(fullLayout.chrome.title!.text).toBe('Full title');
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
it('applies breakpoint override for legend show', () => {
|
|
1285
|
-
const spec = {
|
|
1286
|
-
...lineSpec,
|
|
1287
|
-
legend: { show: true },
|
|
1288
|
-
overrides: {
|
|
1289
|
-
compact: {
|
|
1290
|
-
legend: { show: false },
|
|
1291
|
-
},
|
|
1292
|
-
},
|
|
1293
|
-
};
|
|
1294
|
-
|
|
1295
|
-
// At compact, legend hidden
|
|
1296
|
-
const compactLayout = compileChart(spec, { width: 350, height: 400 });
|
|
1297
|
-
expect(compactLayout.legend.entries).toHaveLength(0);
|
|
1298
|
-
|
|
1299
|
-
// At full, legend shown
|
|
1300
|
-
const fullLayout = compileChart(spec, { width: 800, height: 400 });
|
|
1301
|
-
expect(fullLayout.legend.entries.length).toBeGreaterThan(0);
|
|
1302
|
-
});
|
|
1303
|
-
});
|