@opendata-ai/openchart-engine 8.0.0-rc.8 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +149 -12
- package/dist/index.js +894 -171
- 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 -2339
- 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 -626
- 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 -1450
- 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 -1004
- package/src/geo/compile-map.ts +0 -1341
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- 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 -747
- 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,358 +0,0 @@
|
|
|
1
|
-
import type { ChartLayout } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { HEIGHT_SHORT_MAX } from '@opendata-ai/openchart-core';
|
|
3
|
-
import { afterAll, describe, expect, it } from 'vitest';
|
|
4
|
-
import { compileChart } from '../compile';
|
|
5
|
-
import { checkLayoutInvariants } from './helpers/invariants';
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Seeded PRNG (mulberry32)
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
function mulberry32(seed: number) {
|
|
12
|
-
return () => {
|
|
13
|
-
seed |= 0;
|
|
14
|
-
seed = (seed + 0x6d2b79f5) | 0;
|
|
15
|
-
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
16
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
17
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Constants
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const SEED = Number(process.env.FUZZ_SEED) || 42;
|
|
26
|
-
const CASE_COUNT = 200;
|
|
27
|
-
|
|
28
|
-
// 'column' is a bar chart with swapped encoding axes, not a distinct mark type
|
|
29
|
-
const CHART_VARIANTS = ['bar', 'line', 'point', 'circle', 'column'] as const;
|
|
30
|
-
const WIDTHS = [280, 400, 640, 960] as const;
|
|
31
|
-
const HEIGHTS = [240, 400] as const;
|
|
32
|
-
const LEGEND_POSITIONS = ['top', 'bottom', 'right', 'bottom-right'] as const;
|
|
33
|
-
const CHROME_PRESETS = ['none', 'title-subtitle', 'full'] as const;
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Label pools
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
const SHORT_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
|
40
|
-
const MEDIUM_LABELS = [
|
|
41
|
-
'Sales Q1',
|
|
42
|
-
'Revenue FY23',
|
|
43
|
-
'Customer Count',
|
|
44
|
-
'Net Profit',
|
|
45
|
-
'Gross Margin',
|
|
46
|
-
'Operating Exp',
|
|
47
|
-
'Growth Rate',
|
|
48
|
-
'Market Share',
|
|
49
|
-
];
|
|
50
|
-
const LONG_LABELS = [
|
|
51
|
-
'United States of America Department',
|
|
52
|
-
'Federal Republic of Germany Statistics',
|
|
53
|
-
'Commonwealth of Australia Annual Report',
|
|
54
|
-
'United Kingdom Economic Growth Summary',
|
|
55
|
-
'International Monetary Fund World Outlook',
|
|
56
|
-
'Organization for Economic Cooperation Data',
|
|
57
|
-
];
|
|
58
|
-
const CJK_LABELS = ['中文数据', '日本語テスト', '한국어자료', '数据分析报告', '年度总结'];
|
|
59
|
-
const DIACRITICS_LABELS = [
|
|
60
|
-
'Ünïcödé Dätä',
|
|
61
|
-
'Renseignements généraux',
|
|
62
|
-
'Bevölkerungszählung',
|
|
63
|
-
'Información demográfica',
|
|
64
|
-
'Données économiques',
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
const ALL_LABELS = [
|
|
68
|
-
...SHORT_LABELS,
|
|
69
|
-
...MEDIUM_LABELS,
|
|
70
|
-
...LONG_LABELS,
|
|
71
|
-
...CJK_LABELS,
|
|
72
|
-
...DIACRITICS_LABELS,
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// PRNG helpers
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
function pick<T>(rand: () => number, arr: readonly T[]): T {
|
|
80
|
-
return arr[Math.floor(rand() * arr.length)];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function randInt(rand: () => number, min: number, max: number): number {
|
|
84
|
-
return min + Math.floor(rand() * (max - min + 1));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function pickLabels(rand: () => number, count: number): string[] {
|
|
88
|
-
const labels: string[] = [];
|
|
89
|
-
for (let i = 0; i < count; i++) {
|
|
90
|
-
labels.push(pick(rand, ALL_LABELS));
|
|
91
|
-
}
|
|
92
|
-
return labels;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Chrome generation
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
function generateChrome(rand: () => number, preset: (typeof CHROME_PRESETS)[number]) {
|
|
100
|
-
if (preset === 'none') return undefined;
|
|
101
|
-
if (preset === 'title-subtitle') {
|
|
102
|
-
return {
|
|
103
|
-
title: pick(rand, ['Chart Title', 'Quarterly Earnings', '中文标题', 'Données annuelles']),
|
|
104
|
-
subtitle: pick(rand, ['A subtitle for context', 'Values in millions of USD', '年度摘要数据']),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
// full
|
|
108
|
-
return {
|
|
109
|
-
title: pick(rand, ['Full Chrome Chart', 'Economic Overview', 'Ünïcödé Tïtlé']),
|
|
110
|
-
subtitle: pick(rand, ['Comprehensive analysis', 'Year-over-year comparison']),
|
|
111
|
-
source: pick(rand, ['Source: World Bank', 'Source: 数据来源', 'Source: OECD Statistics']),
|
|
112
|
-
byline: pick(rand, ['By Test Author', 'Données par Équipe']),
|
|
113
|
-
footer: pick(rand, ['Note: values are illustrative', 'All figures adjusted for inflation']),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Data generators per mark type
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
function generateBarData(rand: () => number, hasColor: boolean) {
|
|
122
|
-
const categoryCount = randInt(rand, 3, 8);
|
|
123
|
-
const categories = pickLabels(rand, categoryCount);
|
|
124
|
-
|
|
125
|
-
if (!hasColor) {
|
|
126
|
-
return categories.map((name) => ({
|
|
127
|
-
name,
|
|
128
|
-
value: randInt(rand, 1, 100) + rand() * 50,
|
|
129
|
-
}));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const seriesCount = randInt(rand, 2, 6);
|
|
133
|
-
const seriesNames = pickLabels(rand, seriesCount);
|
|
134
|
-
return categories.flatMap((name) =>
|
|
135
|
-
seriesNames.map((series) => ({
|
|
136
|
-
name,
|
|
137
|
-
value: randInt(rand, 1, 100) + rand() * 50,
|
|
138
|
-
series,
|
|
139
|
-
})),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function generateLineData(rand: () => number, hasColor: boolean) {
|
|
144
|
-
const pointsPerSeries = randInt(rand, 10, 30);
|
|
145
|
-
const baseYear = 2018;
|
|
146
|
-
|
|
147
|
-
if (!hasColor) {
|
|
148
|
-
return Array.from({ length: pointsPerSeries }, (_, i) => ({
|
|
149
|
-
date: `${baseYear + Math.floor(i / 12)}-${String((i % 12) + 1).padStart(2, '0')}-01`,
|
|
150
|
-
value: randInt(rand, 10, 200) + rand() * 50,
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const seriesCount = randInt(rand, 1, 5);
|
|
155
|
-
const seriesNames = pickLabels(rand, seriesCount);
|
|
156
|
-
return seriesNames.flatMap((series) =>
|
|
157
|
-
Array.from({ length: pointsPerSeries }, (_, i) => ({
|
|
158
|
-
date: `${baseYear + Math.floor(i / 12)}-${String((i % 12) + 1).padStart(2, '0')}-01`,
|
|
159
|
-
value: randInt(rand, 10, 200) + rand() * 50,
|
|
160
|
-
series,
|
|
161
|
-
})),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function generateScatterData(rand: () => number, hasColor: boolean) {
|
|
166
|
-
const pointCount = randInt(rand, 20, 50);
|
|
167
|
-
|
|
168
|
-
if (!hasColor) {
|
|
169
|
-
return Array.from({ length: pointCount }, () => ({
|
|
170
|
-
xVal: rand() * 100,
|
|
171
|
-
yVal: rand() * 100,
|
|
172
|
-
}));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const groupCount = randInt(rand, 2, 5);
|
|
176
|
-
const groupNames = pickLabels(rand, groupCount);
|
|
177
|
-
return Array.from({ length: pointCount }, () => ({
|
|
178
|
-
xVal: rand() * 100,
|
|
179
|
-
yVal: rand() * 100,
|
|
180
|
-
group: pick(rand, groupNames),
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function generateDotData(rand: () => number, hasColor: boolean) {
|
|
185
|
-
const categoryCount = randInt(rand, 3, 8);
|
|
186
|
-
const categories = pickLabels(rand, categoryCount);
|
|
187
|
-
|
|
188
|
-
if (!hasColor) {
|
|
189
|
-
return categories.map((name) => ({
|
|
190
|
-
name,
|
|
191
|
-
value: randInt(rand, 1, 100) + rand() * 50,
|
|
192
|
-
}));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const seriesCount = randInt(rand, 2, 4);
|
|
196
|
-
const seriesNames = pickLabels(rand, seriesCount);
|
|
197
|
-
return categories.flatMap((name) =>
|
|
198
|
-
seriesNames.map((series) => ({
|
|
199
|
-
name,
|
|
200
|
-
value: randInt(rand, 1, 100) + rand() * 50,
|
|
201
|
-
series,
|
|
202
|
-
})),
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
// Spec generator
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
|
|
210
|
-
function generateSpec(rand: () => number): {
|
|
211
|
-
spec: Record<string, unknown>;
|
|
212
|
-
width: number;
|
|
213
|
-
height: number;
|
|
214
|
-
} {
|
|
215
|
-
const markType = pick(rand, CHART_VARIANTS);
|
|
216
|
-
const width = pick(rand, WIDTHS);
|
|
217
|
-
const height = pick(rand, HEIGHTS);
|
|
218
|
-
const legendPosition = pick(rand, LEGEND_POSITIONS);
|
|
219
|
-
const chromePreset = pick(rand, CHROME_PRESETS);
|
|
220
|
-
const darkMode = rand() > 0.5;
|
|
221
|
-
const hasColor = rand() > 0.4; // 60% chance of color encoding
|
|
222
|
-
|
|
223
|
-
const chrome = generateChrome(rand, chromePreset);
|
|
224
|
-
|
|
225
|
-
let data: Record<string, unknown>[];
|
|
226
|
-
let encoding: Record<string, unknown>;
|
|
227
|
-
|
|
228
|
-
switch (markType) {
|
|
229
|
-
case 'bar': {
|
|
230
|
-
// Horizontal bar: nominal y, quantitative x
|
|
231
|
-
data = generateBarData(rand, hasColor);
|
|
232
|
-
encoding = {
|
|
233
|
-
x: { field: 'value', type: 'quantitative' },
|
|
234
|
-
y: { field: 'name', type: 'nominal' },
|
|
235
|
-
...(hasColor ? { color: { field: 'series', type: 'nominal' } } : {}),
|
|
236
|
-
};
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
case 'column': {
|
|
240
|
-
// Vertical column (bar with swapped encoding): nominal x, quantitative y
|
|
241
|
-
data = generateBarData(rand, hasColor);
|
|
242
|
-
encoding = {
|
|
243
|
-
x: { field: 'name', type: 'nominal' },
|
|
244
|
-
y: { field: 'value', type: 'quantitative' },
|
|
245
|
-
...(hasColor ? { color: { field: 'series', type: 'nominal' } } : {}),
|
|
246
|
-
};
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
case 'line': {
|
|
250
|
-
data = generateLineData(rand, hasColor);
|
|
251
|
-
encoding = {
|
|
252
|
-
x: { field: 'date', type: 'temporal' },
|
|
253
|
-
y: { field: 'value', type: 'quantitative' },
|
|
254
|
-
...(hasColor ? { color: { field: 'series', type: 'nominal' } } : {}),
|
|
255
|
-
};
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
case 'point': {
|
|
259
|
-
// scatter
|
|
260
|
-
data = generateScatterData(rand, hasColor);
|
|
261
|
-
encoding = {
|
|
262
|
-
x: { field: 'xVal', type: 'quantitative' },
|
|
263
|
-
y: { field: 'yVal', type: 'quantitative' },
|
|
264
|
-
...(hasColor ? { color: { field: 'group', type: 'nominal' } } : {}),
|
|
265
|
-
};
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
case 'circle': {
|
|
269
|
-
// dot
|
|
270
|
-
data = generateDotData(rand, hasColor);
|
|
271
|
-
encoding = {
|
|
272
|
-
x: { field: 'value', type: 'quantitative' },
|
|
273
|
-
y: { field: 'name', type: 'nominal' },
|
|
274
|
-
...(hasColor ? { color: { field: 'series', type: 'nominal' } } : {}),
|
|
275
|
-
};
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const spec: Record<string, unknown> = {
|
|
281
|
-
// 'column' is actually a bar with swapped encoding
|
|
282
|
-
mark: markType === 'column' ? 'bar' : markType,
|
|
283
|
-
data,
|
|
284
|
-
encoding,
|
|
285
|
-
...(chrome ? { chrome } : {}),
|
|
286
|
-
...(hasColor ? { legend: { position: legendPosition } } : {}),
|
|
287
|
-
...(darkMode ? { darkMode: 'force' } : {}),
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
return { spec, width, height };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
// Fuzz test
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
|
|
297
|
-
// Short-container threshold below which the rotated-label starvation family is
|
|
298
|
-
// tolerated (see the legend-overflow allowlist below).
|
|
299
|
-
const STARVATION_HEIGHT_MAX = HEIGHT_SHORT_MAX;
|
|
300
|
-
|
|
301
|
-
// Known engine bugs that the fuzz legitimately catches. Filter these so the
|
|
302
|
-
// suite passes while still failing on NEW violation types. `ctx` scopes the
|
|
303
|
-
// space-starvation allowlist so ordinary-size regressions still fail.
|
|
304
|
-
function isKnownViolation(v: string, ctx: { height: number; rotatedTicks: boolean }): boolean {
|
|
305
|
-
if (v.startsWith('legend entry ') || v.startsWith('legend-entry-')) return true;
|
|
306
|
-
if (v.startsWith('x-axis-labels outside SVG viewport')) return true;
|
|
307
|
-
// Space starvation at extreme aspect ratios: a very short container (e.g.
|
|
308
|
-
// 240px tall) with title chrome + auto-rotated long category labels + a
|
|
309
|
-
// bottom legend cannot fit everything. Reserving the correct rotated-label
|
|
310
|
-
// extent (textWidth*|sin θ| + lineHeight*|cos θ|) pushes the bottom legend
|
|
311
|
-
// past the viewport. The previous under-reservation only "fit" by letting
|
|
312
|
-
// rotated ticks overlap the legend/source, which was the very bug the extent
|
|
313
|
-
// fix addresses. Only tolerate it in that family (short container AND rotated
|
|
314
|
-
// ticks present); an ordinary-size legend overflow is a real regression.
|
|
315
|
-
if (v.startsWith('legend outside SVG viewport')) {
|
|
316
|
-
return ctx.height <= STARVATION_HEIGHT_MAX && ctx.rotatedTicks;
|
|
317
|
-
}
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
let compileSkipCount = 0;
|
|
322
|
-
|
|
323
|
-
describe('layout fuzz', () => {
|
|
324
|
-
const rand = mulberry32(SEED);
|
|
325
|
-
|
|
326
|
-
afterAll(() => {
|
|
327
|
-
if (compileSkipCount > 0) {
|
|
328
|
-
console.warn(`layout fuzz: ${compileSkipCount} cases skipped due to compile errors`);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
for (let i = 0; i < CASE_COUNT; i++) {
|
|
333
|
-
const { spec, width, height } = generateSpec(rand);
|
|
334
|
-
|
|
335
|
-
it(`fuzz case ${i} (seed=${SEED})`, () => {
|
|
336
|
-
let layout: ChartLayout;
|
|
337
|
-
try {
|
|
338
|
-
layout = compileChart(spec, { width, height });
|
|
339
|
-
} catch {
|
|
340
|
-
compileSkipCount++;
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const violations = checkLayoutInvariants(layout, { svgWidth: width, svgHeight: height });
|
|
344
|
-
const rotatedTicks = Math.abs(layout.axes.x?.tickAngle ?? 0) > 10;
|
|
345
|
-
const novel = violations.filter((v) => !isKnownViolation(v, { height, rotatedTicks }));
|
|
346
|
-
if (novel.length > 0) {
|
|
347
|
-
const msg = [
|
|
348
|
-
`Seed: ${SEED}, Case: ${i}`,
|
|
349
|
-
`Spec: ${JSON.stringify(spec, null, 2)}`,
|
|
350
|
-
`Size: ${width}x${height}`,
|
|
351
|
-
'Novel violations:',
|
|
352
|
-
...novel,
|
|
353
|
-
].join('\n');
|
|
354
|
-
expect.fail(msg);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
});
|