@opendata-ai/openchart-engine 8.0.0-rc.9 → 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 +804 -155
- 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,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
|
-
}
|