@opendata-ai/openchart-engine 8.0.0-rc.10 → 8.0.0-rc.12
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 +7 -0
- package/dist/index.js +123 -54
- 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 -359
- 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 -767
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1548
- 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 -731
- 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 -2093
- 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,359 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { compileChart } from '../compile';
|
|
3
|
-
import { autoColumns, computeFacetGrid } from '../layout/facet';
|
|
4
|
-
|
|
5
|
-
const facetData = [
|
|
6
|
-
{ year: '2020', country: 'US', value: 100 },
|
|
7
|
-
{ year: '2021', country: 'US', value: 120 },
|
|
8
|
-
{ year: '2020', country: 'UK', value: 80 },
|
|
9
|
-
{ year: '2021', country: 'UK', value: 90 },
|
|
10
|
-
{ year: '2020', country: 'DE', value: 70 },
|
|
11
|
-
{ year: '2021', country: 'DE', value: 85 },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const facetSpec = {
|
|
15
|
-
mark: 'line' as const,
|
|
16
|
-
data: facetData,
|
|
17
|
-
encoding: {
|
|
18
|
-
x: { field: 'year', type: 'ordinal' as const },
|
|
19
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
20
|
-
facet: { field: 'country', type: 'nominal' as const },
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
describe('autoColumns', () => {
|
|
25
|
-
it('returns sqrt-based columns for moderate panel counts', () => {
|
|
26
|
-
expect(autoColumns(4, 800)).toBe(2);
|
|
27
|
-
expect(autoColumns(9, 1200)).toBe(3);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('caps columns by available width', () => {
|
|
31
|
-
expect(autoColumns(9, 400)).toBe(2);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('returns 1 for single panel', () => {
|
|
35
|
-
expect(autoColumns(1, 800)).toBe(1);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('does not exceed panel count', () => {
|
|
39
|
-
expect(autoColumns(2, 2000)).toBe(2);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('computeFacetGrid', () => {
|
|
44
|
-
it('returns empty result for 0 panels', () => {
|
|
45
|
-
const result = computeFacetGrid(
|
|
46
|
-
[],
|
|
47
|
-
undefined,
|
|
48
|
-
{ x: 0, y: 0, width: 600, height: 400 },
|
|
49
|
-
{
|
|
50
|
-
left: 40,
|
|
51
|
-
bottom: 30,
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
expect(result.panels).toEqual([]);
|
|
55
|
-
expect(result.columns).toBe(0);
|
|
56
|
-
expect(result.rows).toBe(0);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('computes correct number of panels', () => {
|
|
60
|
-
const result = computeFacetGrid(
|
|
61
|
-
['A', 'B', 'C', 'D'],
|
|
62
|
-
2,
|
|
63
|
-
{ x: 0, y: 0, width: 600, height: 400 },
|
|
64
|
-
{ left: 40, bottom: 30 },
|
|
65
|
-
);
|
|
66
|
-
expect(result.panels).toHaveLength(4);
|
|
67
|
-
expect(result.columns).toBe(2);
|
|
68
|
-
expect(result.rows).toBe(2);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('assigns correct row and col indices', () => {
|
|
72
|
-
const result = computeFacetGrid(
|
|
73
|
-
['A', 'B', 'C'],
|
|
74
|
-
2,
|
|
75
|
-
{ x: 0, y: 0, width: 600, height: 400 },
|
|
76
|
-
{ left: 40, bottom: 30 },
|
|
77
|
-
);
|
|
78
|
-
expect(result.panels[0]).toMatchObject({ key: 'A', row: 0, col: 0 });
|
|
79
|
-
expect(result.panels[1]).toMatchObject({ key: 'B', row: 0, col: 1 });
|
|
80
|
-
expect(result.panels[2]).toMatchObject({ key: 'C', row: 1, col: 0 });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('panels do not overlap', () => {
|
|
84
|
-
const result = computeFacetGrid(
|
|
85
|
-
['A', 'B', 'C', 'D', 'E', 'F'],
|
|
86
|
-
3,
|
|
87
|
-
{ x: 0, y: 0, width: 800, height: 600 },
|
|
88
|
-
{ left: 40, bottom: 30 },
|
|
89
|
-
);
|
|
90
|
-
for (let i = 0; i < result.panels.length; i++) {
|
|
91
|
-
for (let j = i + 1; j < result.panels.length; j++) {
|
|
92
|
-
const a = result.panels[i].area;
|
|
93
|
-
const b = result.panels[j].area;
|
|
94
|
-
const xOverlap = a.x < b.x + b.width && a.x + a.width > b.x;
|
|
95
|
-
const yOverlap = a.y < b.y + b.height && a.y + a.height > b.y;
|
|
96
|
-
expect(xOverlap && yOverlap).toBe(false);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('degrades columns when panels would be too narrow', () => {
|
|
102
|
-
const result = computeFacetGrid(
|
|
103
|
-
['A', 'B', 'C', 'D'],
|
|
104
|
-
4,
|
|
105
|
-
{ x: 0, y: 0, width: 400, height: 400 },
|
|
106
|
-
{ left: 40, bottom: 30 },
|
|
107
|
-
);
|
|
108
|
-
expect(result.columns).toBeLessThan(4);
|
|
109
|
-
for (const panel of result.panels) {
|
|
110
|
-
expect(panel.area.width).toBeGreaterThanOrEqual(0);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('clamps panel height to zero for tiny containers', () => {
|
|
115
|
-
const result = computeFacetGrid(
|
|
116
|
-
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
|
|
117
|
-
1,
|
|
118
|
-
{ x: 0, y: 0, width: 400, height: 100 },
|
|
119
|
-
{ left: 40, bottom: 30 },
|
|
120
|
-
);
|
|
121
|
-
for (const panel of result.panels) {
|
|
122
|
-
expect(panel.area.height).toBeGreaterThanOrEqual(0);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('compileChart with facet', () => {
|
|
128
|
-
it('populates layout.facet with correct panel count', () => {
|
|
129
|
-
const layout = compileChart(facetSpec, { width: 600, height: 400 });
|
|
130
|
-
expect(layout.facet).toBeDefined();
|
|
131
|
-
expect(layout.facet!.panels).toHaveLength(3);
|
|
132
|
-
expect(layout.facet!.facetField).toBe('country');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('sorts panels ascending by default', () => {
|
|
136
|
-
const layout = compileChart(facetSpec, { width: 600, height: 400 });
|
|
137
|
-
const keys = layout.facet!.panels.map((p) => p.key);
|
|
138
|
-
expect(keys).toEqual(['DE', 'UK', 'US']);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('respects descending sort', () => {
|
|
142
|
-
const spec = {
|
|
143
|
-
...facetSpec,
|
|
144
|
-
encoding: {
|
|
145
|
-
...facetSpec.encoding,
|
|
146
|
-
facet: { ...facetSpec.encoding.facet, sort: 'descending' as const },
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
150
|
-
const keys = layout.facet!.panels.map((p) => p.key);
|
|
151
|
-
expect(keys).toEqual(['US', 'UK', 'DE']);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('preserves data order with sort: null', () => {
|
|
155
|
-
const spec = {
|
|
156
|
-
...facetSpec,
|
|
157
|
-
encoding: {
|
|
158
|
-
...facetSpec.encoding,
|
|
159
|
-
facet: { ...facetSpec.encoding.facet, sort: null },
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
163
|
-
const keys = layout.facet!.panels.map((p) => p.key);
|
|
164
|
-
expect(keys).toEqual(['US', 'UK', 'DE']);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('uses shared scales by default', () => {
|
|
168
|
-
const layout = compileChart(facetSpec, { width: 600, height: 400 });
|
|
169
|
-
expect(layout.facet!.sharedScales).toBe(true);
|
|
170
|
-
expect(layout.facet!.direction).toBe('wrap');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('supports independent y scales via resolve', () => {
|
|
174
|
-
const spec = {
|
|
175
|
-
...facetSpec,
|
|
176
|
-
resolve: { scale: { y: 'independent' as const } },
|
|
177
|
-
};
|
|
178
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
179
|
-
expect(layout.facet!.sharedScales).toBe(false);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('each panel has marks and axes', () => {
|
|
183
|
-
const layout = compileChart(facetSpec, { width: 600, height: 400 });
|
|
184
|
-
for (const panel of layout.facet!.panels) {
|
|
185
|
-
expect(panel.marks.length).toBeGreaterThan(0);
|
|
186
|
-
expect(panel.area.width).toBeGreaterThan(0);
|
|
187
|
-
expect(panel.area.height).toBeGreaterThan(0);
|
|
188
|
-
expect(panel.header.text).toBeTruthy();
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('figure-level axes are undefined when faceted', () => {
|
|
193
|
-
const layout = compileChart(facetSpec, { width: 600, height: 400 });
|
|
194
|
-
expect(layout.axes.x).toBeUndefined();
|
|
195
|
-
expect(layout.axes.y).toBeUndefined();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('marks union includes all panel marks', () => {
|
|
199
|
-
const layout = compileChart(facetSpec, { width: 600, height: 400 });
|
|
200
|
-
const panelMarkCount = layout.facet!.panels.reduce((sum, p) => sum + p.marks.length, 0);
|
|
201
|
-
expect(layout.marks.length).toBe(panelMarkCount);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('non-faceted spec produces no facet layout', () => {
|
|
205
|
-
const spec = {
|
|
206
|
-
mark: 'line' as const,
|
|
207
|
-
data: facetData,
|
|
208
|
-
encoding: {
|
|
209
|
-
x: { field: 'year', type: 'ordinal' as const },
|
|
210
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
214
|
-
expect(layout.facet).toBeUndefined();
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// Row faceting
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
|
|
222
|
-
const rowSpec = {
|
|
223
|
-
mark: 'bar' as const,
|
|
224
|
-
data: facetData,
|
|
225
|
-
encoding: {
|
|
226
|
-
x: { field: 'value', type: 'quantitative' as const },
|
|
227
|
-
y: { field: 'country', type: 'nominal' as const },
|
|
228
|
-
row: { field: 'year', type: 'nominal' as const },
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
describe('compileChart with row faceting', () => {
|
|
233
|
-
it('produces facet layout with direction row', () => {
|
|
234
|
-
const layout = compileChart(rowSpec, { width: 600, height: 400 });
|
|
235
|
-
expect(layout.facet).toBeDefined();
|
|
236
|
-
expect(layout.facet!.direction).toBe('row');
|
|
237
|
-
expect(layout.facet!.columns).toBe(1);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('creates one panel per unique row value', () => {
|
|
241
|
-
const layout = compileChart(rowSpec, { width: 600, height: 400 });
|
|
242
|
-
expect(layout.facet!.panels).toHaveLength(2);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('defaults x to shared, y to independent', () => {
|
|
246
|
-
const layout = compileChart(rowSpec, { width: 600, height: 400 });
|
|
247
|
-
expect(layout.facet!.sharedScales).toBe(false);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('strips x-axis from non-bottom panels', () => {
|
|
251
|
-
const layout = compileChart(rowSpec, { width: 600, height: 600 });
|
|
252
|
-
const panels = layout.facet!.panels;
|
|
253
|
-
expect(panels[0].axes.x?.ticks ?? []).toHaveLength(0);
|
|
254
|
-
const bottomPanel = panels[panels.length - 1];
|
|
255
|
-
expect(bottomPanel.axes.x?.ticks.length).toBeGreaterThan(0);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('keeps y-axis on all panels', () => {
|
|
259
|
-
const layout = compileChart(rowSpec, { width: 600, height: 600 });
|
|
260
|
-
for (const panel of layout.facet!.panels) {
|
|
261
|
-
expect(panel.axes.y).toBeDefined();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('left-aligns headers', () => {
|
|
266
|
-
const layout = compileChart(rowSpec, { width: 600, height: 400 });
|
|
267
|
-
for (const panel of layout.facet!.panels) {
|
|
268
|
-
expect(panel.header.textAnchor).toBe('start');
|
|
269
|
-
expect(panel.header.x).toBe(panel.area.x);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('allows overriding y to shared via resolve', () => {
|
|
274
|
-
const spec = {
|
|
275
|
-
...rowSpec,
|
|
276
|
-
resolve: { scale: { y: 'shared' as const } },
|
|
277
|
-
};
|
|
278
|
-
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
279
|
-
expect(layout.facet!.sharedScales).toBe(true);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('each panel has marks', () => {
|
|
283
|
-
const layout = compileChart(rowSpec, { width: 600, height: 600 });
|
|
284
|
-
for (const panel of layout.facet!.panels) {
|
|
285
|
-
expect(panel.marks.length).toBeGreaterThan(0);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// ---------------------------------------------------------------------------
|
|
291
|
-
// Column faceting
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
|
|
294
|
-
const colSpec = {
|
|
295
|
-
mark: 'bar' as const,
|
|
296
|
-
data: facetData,
|
|
297
|
-
encoding: {
|
|
298
|
-
x: { field: 'country', type: 'nominal' as const },
|
|
299
|
-
y: { field: 'value', type: 'quantitative' as const },
|
|
300
|
-
column: { field: 'year', type: 'nominal' as const },
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
describe('compileChart with column faceting', () => {
|
|
305
|
-
it('produces facet layout with direction column', () => {
|
|
306
|
-
const layout = compileChart(colSpec, { width: 800, height: 400 });
|
|
307
|
-
expect(layout.facet).toBeDefined();
|
|
308
|
-
expect(layout.facet!.direction).toBe('column');
|
|
309
|
-
expect(layout.facet!.columns).toBe(2);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('defaults y to shared, x to independent', () => {
|
|
313
|
-
const layout = compileChart(colSpec, { width: 800, height: 400 });
|
|
314
|
-
expect(layout.facet!.sharedScales).toBe(false);
|
|
315
|
-
// Verify the directional resolve: y-axis ticks should be stripped from
|
|
316
|
-
// non-leftmost panels (shared y), x-axis ticks should appear on all panels
|
|
317
|
-
// (independent x).
|
|
318
|
-
const panels = layout.facet!.panels;
|
|
319
|
-
if (panels.length > 1) {
|
|
320
|
-
const rightPanel = panels[1];
|
|
321
|
-
expect(rightPanel.axes.y?.ticks ?? []).toHaveLength(0);
|
|
322
|
-
}
|
|
323
|
-
const leftPanel = panels[0];
|
|
324
|
-
expect(leftPanel.axes.y?.ticks?.length).toBeGreaterThan(0);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('strips y-axis from non-leftmost panels', () => {
|
|
328
|
-
const layout = compileChart(colSpec, { width: 800, height: 400 });
|
|
329
|
-
const panels = layout.facet!.panels;
|
|
330
|
-
expect(panels[0].axes.y?.ticks?.length).toBeGreaterThan(0);
|
|
331
|
-
if (panels.length > 1) {
|
|
332
|
-
expect(panels[1].axes.y?.ticks ?? []).toHaveLength(0);
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('allows overriding x to shared via resolve', () => {
|
|
337
|
-
const spec = {
|
|
338
|
-
...colSpec,
|
|
339
|
-
resolve: { scale: { x: 'shared' as const } },
|
|
340
|
-
};
|
|
341
|
-
const layout = compileChart(spec, { width: 800, height: 400 });
|
|
342
|
-
expect(layout.facet!.sharedScales).toBe(true);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('centers headers (not left-aligned)', () => {
|
|
346
|
-
const layout = compileChart(colSpec, { width: 800, height: 400 });
|
|
347
|
-
for (const panel of layout.facet!.panels) {
|
|
348
|
-
expect(panel.header.textAnchor).toBeUndefined();
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('creates panels in a single row', () => {
|
|
353
|
-
const layout = compileChart(colSpec, { width: 800, height: 400 });
|
|
354
|
-
for (const panel of layout.facet!.panels) {
|
|
355
|
-
expect(panel.header.text).toBeTruthy();
|
|
356
|
-
}
|
|
357
|
-
expect(layout.facet!.panels).toHaveLength(2);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout invariant assertions.
|
|
3
|
-
*
|
|
4
|
-
* Extracts bounding boxes from a compiled ChartLayout and checks that
|
|
5
|
-
* layout elements don't overlap where they shouldn't.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
CategoricalLegendLayout,
|
|
10
|
-
ChartLayout,
|
|
11
|
-
LegendLayout,
|
|
12
|
-
Rect,
|
|
13
|
-
} from '@opendata-ai/openchart-core';
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Types
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
export interface NamedRect extends Rect {
|
|
20
|
-
name: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Type guards
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
function isCategoricalLegend(legend: LegendLayout): legend is CategoricalLegendLayout {
|
|
28
|
-
return 'entries' in legend && (!legend.type || legend.type === 'categorical');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Every legend on the layout. Falls back to the singular `legend` slot so this
|
|
33
|
-
* still works against layouts built by older code paths (and by tests that
|
|
34
|
-
* hand-assemble a ChartLayout).
|
|
35
|
-
*/
|
|
36
|
-
function allLegends(layout: ChartLayout): LegendLayout[] {
|
|
37
|
-
if (layout.legends?.length) return layout.legends;
|
|
38
|
-
return layout.legend ? [layout.legend] : [];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Box extraction
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
const CHROME_KEYS = [
|
|
46
|
-
'eyebrow',
|
|
47
|
-
'title',
|
|
48
|
-
'subtitle',
|
|
49
|
-
'source',
|
|
50
|
-
'byline',
|
|
51
|
-
'footer',
|
|
52
|
-
'brand',
|
|
53
|
-
] as const;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Extracts every layout element's bounding box as a NamedRect.
|
|
57
|
-
*/
|
|
58
|
-
export function extractBoxes(layout: ChartLayout): NamedRect[] {
|
|
59
|
-
const boxes: NamedRect[] = [];
|
|
60
|
-
|
|
61
|
-
// Chart area
|
|
62
|
-
boxes.push({ ...layout.area, name: 'chartArea' });
|
|
63
|
-
|
|
64
|
-
// Legend bounds -- EVERY legend, not just the primary. A chart can carry both
|
|
65
|
-
// a color legend and a size legend; registering only `layout.legend` would
|
|
66
|
-
// leave the overlap invariants blind to exactly the element most likely to be
|
|
67
|
-
// mis-reserved, and they would pass a chart whose size legend sits on the plot.
|
|
68
|
-
for (const legend of allLegends(layout)) {
|
|
69
|
-
const suffix = legend.channel && legend.channel !== 'color' ? `-${legend.channel}` : '';
|
|
70
|
-
boxes.push({ ...legend.bounds, name: `legend${suffix}` });
|
|
71
|
-
|
|
72
|
-
// Legend entry positions (categorical only)
|
|
73
|
-
if (isCategoricalLegend(legend) && legend.entryPositions) {
|
|
74
|
-
const rowHeight = legend.rowHeight ?? 16;
|
|
75
|
-
for (let i = 0; i < legend.entryPositions.length; i++) {
|
|
76
|
-
const pos = legend.entryPositions[i];
|
|
77
|
-
boxes.push({
|
|
78
|
-
x: pos.x,
|
|
79
|
-
y: pos.y,
|
|
80
|
-
width: pos.width,
|
|
81
|
-
height: rowHeight,
|
|
82
|
-
name: `legend${suffix}-entry-${i}`,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// X-axis label band
|
|
89
|
-
if (layout.axes.x?.extent && layout.axes.x.extent > 0) {
|
|
90
|
-
boxes.push({
|
|
91
|
-
x: layout.area.x,
|
|
92
|
-
y: layout.area.y + layout.area.height,
|
|
93
|
-
width: layout.area.width,
|
|
94
|
-
height: layout.axes.x.extent,
|
|
95
|
-
name: 'x-axis-labels',
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Y-axis label band
|
|
100
|
-
if (layout.axes.y?.extent && layout.axes.y.extent > 0) {
|
|
101
|
-
boxes.push({
|
|
102
|
-
x: layout.area.x - layout.axes.y.extent,
|
|
103
|
-
y: layout.area.y,
|
|
104
|
-
width: layout.axes.y.extent,
|
|
105
|
-
height: layout.area.height,
|
|
106
|
-
name: 'y-axis-labels',
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Chrome elements store y as the TOP edge of the text box (renderers convert
|
|
111
|
-
// to the alphabetic baseline via textAscent), so y is the box top directly.
|
|
112
|
-
for (const key of CHROME_KEYS) {
|
|
113
|
-
const el = layout.chrome[key];
|
|
114
|
-
if (el) {
|
|
115
|
-
const h = el.style.fontSize * el.style.lineHeight;
|
|
116
|
-
boxes.push({ x: el.x, y: el.y, width: el.maxWidth, height: h, name: key });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Annotation bounds
|
|
121
|
-
for (let i = 0; i < layout.annotations.length; i++) {
|
|
122
|
-
const annotation = layout.annotations[i];
|
|
123
|
-
if (annotation.bounds) {
|
|
124
|
-
boxes.push({ ...annotation.bounds, name: `annotation-${i}` });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Endpoint labels
|
|
129
|
-
if (layout.endpointLabels?.bounds) {
|
|
130
|
-
boxes.push({ ...layout.endpointLabels.bounds, name: 'endpoint-labels' });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return boxes;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Overlap check
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Throws if two rects overlap by more than epsilon pixels.
|
|
142
|
-
*/
|
|
143
|
-
export function assertNoOverlap(a: NamedRect, b: NamedRect, epsilon = 0.5): void {
|
|
144
|
-
const overlaps =
|
|
145
|
-
a.x < b.x + b.width - epsilon &&
|
|
146
|
-
a.x + a.width > b.x + epsilon &&
|
|
147
|
-
a.y < b.y + b.height - epsilon &&
|
|
148
|
-
a.y + a.height > b.y + epsilon;
|
|
149
|
-
|
|
150
|
-
if (overlaps) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`Overlap detected between "${a.name}" and "${b.name}": ` +
|
|
153
|
-
`a=${JSON.stringify({ x: a.x, y: a.y, width: a.width, height: a.height })}, ` +
|
|
154
|
-
`b=${JSON.stringify({ x: b.x, y: b.y, width: b.width, height: b.height })}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Containment helper
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
function isContained(inner: Rect, outer: Rect, tolerance: number): boolean {
|
|
164
|
-
return (
|
|
165
|
-
inner.x >= outer.x - tolerance &&
|
|
166
|
-
inner.y >= outer.y - tolerance &&
|
|
167
|
-
inner.x + inner.width <= outer.x + outer.width + tolerance &&
|
|
168
|
-
inner.y + inner.height <= outer.y + outer.height + tolerance
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function rectsOverlap(a: Rect, b: Rect, epsilon: number): boolean {
|
|
173
|
-
return (
|
|
174
|
-
a.x < b.x + b.width - epsilon &&
|
|
175
|
-
a.x + a.width > b.x + epsilon &&
|
|
176
|
-
a.y < b.y + b.height - epsilon &&
|
|
177
|
-
a.y + a.height > b.y + epsilon
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function formatRect(r: Rect): string {
|
|
182
|
-
return JSON.stringify({ x: r.x, y: r.y, width: r.width, height: r.height });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
// Full invariant check
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Returns a list of violation strings. Empty when clean.
|
|
191
|
-
*/
|
|
192
|
-
export function checkLayoutInvariants(
|
|
193
|
-
layout: ChartLayout,
|
|
194
|
-
opts: { svgWidth: number; svgHeight: number },
|
|
195
|
-
): string[] {
|
|
196
|
-
const violations: string[] = [];
|
|
197
|
-
|
|
198
|
-
// 1. No legend intrudes into chartArea (top/bottom only; right/bottom-right
|
|
199
|
-
// use area.width reduction, so overlap is a horizontal concern checked by rule 4)
|
|
200
|
-
//
|
|
201
|
-
// Checked for EVERY legend and every legend type. A size legend is not
|
|
202
|
-
// categorical, and it is the one whose space is most likely to be
|
|
203
|
-
// under-reserved -- gating this on `isCategoricalLegend` would skip it.
|
|
204
|
-
for (const legend of allLegends(layout)) {
|
|
205
|
-
if (legend.bounds.width <= 0 || legend.bounds.height <= 0) continue;
|
|
206
|
-
const pos = legend.position;
|
|
207
|
-
if (pos === 'top' || pos === 'bottom') {
|
|
208
|
-
if (rectsOverlap(legend.bounds, layout.area, 0.5)) {
|
|
209
|
-
const name = legend.channel ?? 'color';
|
|
210
|
-
violations.push(
|
|
211
|
-
`${name} legend intrudes into chartArea: legend=${formatRect(legend.bounds)} chartArea=${formatRect(layout.area)}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 1b. Legends don't overlap each other.
|
|
218
|
-
const legends = allLegends(layout).filter((l) => l.bounds.width > 0 && l.bounds.height > 0);
|
|
219
|
-
for (let i = 0; i < legends.length; i++) {
|
|
220
|
-
for (let j = i + 1; j < legends.length; j++) {
|
|
221
|
-
if (rectsOverlap(legends[i].bounds, legends[j].bounds, 0.5)) {
|
|
222
|
-
violations.push(
|
|
223
|
-
`legends overlap: ${legends[i].channel ?? 'color'}=${formatRect(legends[i].bounds)} ${legends[j].channel ?? 'color'}=${formatRect(legends[j].bounds)}`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 2. Every legend entryPosition box lies inside legend bounds
|
|
230
|
-
if (isCategoricalLegend(layout.legend) && layout.legend.entryPositions) {
|
|
231
|
-
const rowHeight = layout.legend.rowHeight ?? 16;
|
|
232
|
-
// Entry width includes trailing entryGap; the last entry in a row doesn't
|
|
233
|
-
// need that gap to fit inside bounds, so allow entryGap as extra tolerance.
|
|
234
|
-
const gapTolerance = layout.legend.entryGap + 1;
|
|
235
|
-
for (let i = 0; i < layout.legend.entryPositions.length; i++) {
|
|
236
|
-
const pos = layout.legend.entryPositions[i];
|
|
237
|
-
const entryBox: Rect = {
|
|
238
|
-
x: pos.x,
|
|
239
|
-
y: pos.y,
|
|
240
|
-
width: pos.width,
|
|
241
|
-
height: rowHeight,
|
|
242
|
-
};
|
|
243
|
-
if (!isContained(entryBox, layout.legend.bounds, gapTolerance)) {
|
|
244
|
-
violations.push(
|
|
245
|
-
`legend entry ${i} outside legend bounds: entry=${formatRect(entryBox)} bounds=${formatRect(layout.legend.bounds)}`,
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// 3. X-axis label band doesn't intersect bottom chrome
|
|
252
|
-
const xExtent = layout.axes.x?.extent;
|
|
253
|
-
if (xExtent && xExtent > 0) {
|
|
254
|
-
const xAxisBand: Rect = {
|
|
255
|
-
x: layout.area.x,
|
|
256
|
-
y: layout.area.y + layout.area.height,
|
|
257
|
-
width: layout.area.width,
|
|
258
|
-
height: xExtent,
|
|
259
|
-
};
|
|
260
|
-
const bottomChromeKeys = ['source', 'byline', 'footer'] as const;
|
|
261
|
-
for (const key of bottomChromeKeys) {
|
|
262
|
-
const el = layout.chrome[key];
|
|
263
|
-
if (el) {
|
|
264
|
-
const h = el.style.fontSize * el.style.lineHeight;
|
|
265
|
-
const chromeBox: Rect = { x: el.x, y: el.y, width: el.maxWidth, height: h };
|
|
266
|
-
if (rectsOverlap(xAxisBand, chromeBox, 0.5)) {
|
|
267
|
-
violations.push(
|
|
268
|
-
`x-axis labels intersect ${key}: xAxis=${formatRect(xAxisBand)} ${key}=${formatRect(chromeBox)}`,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 4. All boxes stay within SVG viewport
|
|
276
|
-
const viewport: Rect = {
|
|
277
|
-
x: 0,
|
|
278
|
-
y: 0,
|
|
279
|
-
width: opts.svgWidth,
|
|
280
|
-
height: opts.svgHeight,
|
|
281
|
-
};
|
|
282
|
-
const allBoxes = extractBoxes(layout);
|
|
283
|
-
for (const box of allBoxes) {
|
|
284
|
-
if (!isContained(box, viewport, 2)) {
|
|
285
|
-
violations.push(
|
|
286
|
-
`${box.name} outside SVG viewport: box=${formatRect(box)} viewport=${formatRect(viewport)}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// 5. Reserved margins cover drawn content
|
|
292
|
-
if (layout.area.y < layout.chrome.topHeight - 1) {
|
|
293
|
-
violations.push(
|
|
294
|
-
`top chrome overflows into chart area: area.y=${layout.area.y} chrome.topHeight=${layout.chrome.topHeight}`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return violations;
|
|
299
|
-
}
|