@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,348 +0,0 @@
|
|
|
1
|
-
import type { ChartSpec, SizeLegendLayout } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { compileChart } from '../../compile';
|
|
4
|
-
|
|
5
|
-
const OPTIONS = { width: 800, height: 500 };
|
|
6
|
-
|
|
7
|
-
/** Gapminder-shaped: a bubble scatter keying BOTH color (nominal) and size (quantitative). */
|
|
8
|
-
const bubbles: ChartSpec = {
|
|
9
|
-
mark: { type: 'point' },
|
|
10
|
-
data: [
|
|
11
|
-
{ gdp: 1200, life: 55, pop: 300_000, region: 'Africa' },
|
|
12
|
-
{ gdp: 8000, life: 70, pop: 50_000_000, region: 'Asia' },
|
|
13
|
-
{ gdp: 45000, life: 81, pop: 8_000_000, region: 'Europe' },
|
|
14
|
-
{ gdp: 60000, life: 79, pop: 330_000_000, region: 'Americas' },
|
|
15
|
-
{ gdp: 30000, life: 77, pop: 1_400_000_000, region: 'Asia' },
|
|
16
|
-
],
|
|
17
|
-
encoding: {
|
|
18
|
-
x: { field: 'gdp', type: 'quantitative' },
|
|
19
|
-
y: { field: 'life', type: 'quantitative' },
|
|
20
|
-
color: { field: 'region', type: 'nominal' },
|
|
21
|
-
size: { field: 'pop', type: 'quantitative' },
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/** Beeswarm: same size channel, but a [2, 10] radius range -- a 20px-tall stack. */
|
|
26
|
-
const swarmSpec: ChartSpec = {
|
|
27
|
-
mark: { type: 'beeswarm' },
|
|
28
|
-
data: [...(bubbles.data as object[])] as ChartSpec['data'],
|
|
29
|
-
encoding: {
|
|
30
|
-
x: { field: 'gdp', type: 'quantitative' },
|
|
31
|
-
y: { field: 'region', type: 'nominal' },
|
|
32
|
-
size: { field: 'pop', type: 'quantitative' },
|
|
33
|
-
},
|
|
34
|
-
} as ChartSpec;
|
|
35
|
-
|
|
36
|
-
function sizeLegendOf(spec: ChartSpec, options = OPTIONS): SizeLegendLayout | undefined {
|
|
37
|
-
const layout = compileChart(spec, options);
|
|
38
|
-
return layout.legends.find((l): l is SizeLegendLayout => l.type === 'size');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('size legend', () => {
|
|
42
|
-
/**
|
|
43
|
-
* The whole point of the plural legend slot. A bubble chart keys continent
|
|
44
|
-
* (color) and population (size); the single `legend` slot could only ever hold
|
|
45
|
-
* one, so adding a size legend would have silently deleted the color legend.
|
|
46
|
-
*/
|
|
47
|
-
it('renders alongside the color legend, not instead of it', () => {
|
|
48
|
-
const layout = compileChart(bubbles, OPTIONS);
|
|
49
|
-
|
|
50
|
-
expect(layout.legends).toHaveLength(2);
|
|
51
|
-
const channels = layout.legends.map((l) => l.channel ?? 'color');
|
|
52
|
-
expect(channels).toContain('color');
|
|
53
|
-
expect(channels).toContain('size');
|
|
54
|
-
|
|
55
|
-
// The primary slot still holds the color legend, for every existing consumer.
|
|
56
|
-
expect(layout.legend.type === 'size').toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('is absent when nothing encodes size', () => {
|
|
60
|
-
const noSize: ChartSpec = {
|
|
61
|
-
...bubbles,
|
|
62
|
-
encoding: { ...bubbles.encoding, size: undefined },
|
|
63
|
-
} as ChartSpec;
|
|
64
|
-
expect(sizeLegendOf(noSize)).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The key must resolve the SAME scale the marks resolve, or its circles are
|
|
69
|
-
* not the size of the bubbles they claim to explain. Both call buildSizeScale.
|
|
70
|
-
*/
|
|
71
|
-
it('draws circles at the radius the marks actually use', () => {
|
|
72
|
-
const layout = compileChart(bubbles, OPTIONS);
|
|
73
|
-
const legend = layout.legends.find((l): l is SizeLegendLayout => l.type === 'size');
|
|
74
|
-
const points = layout.marks.filter((m) => m.type === 'point');
|
|
75
|
-
|
|
76
|
-
const maxMarkRadius = Math.max(...points.map((p) => (p as { r: number }).r));
|
|
77
|
-
const maxCircleRadius = Math.max(...(legend?.circles ?? []).map((c) => c.radius));
|
|
78
|
-
|
|
79
|
-
// A scatter's radii ([3, 30]) are already legible, so the key is drawn at the
|
|
80
|
-
// marks' true sizes: the largest circle matches the biggest bubble exactly.
|
|
81
|
-
expect(maxCircleRadius).toBeCloseTo(maxMarkRadius, 5);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The one place the circles are allowed to depart from the marks. A beeswarm
|
|
86
|
-
* sizes its dots [2, 10]; drawn literally, the key is a 20px smudge. Scale the
|
|
87
|
-
* swatch up so it reads -- the labels still carry the true values.
|
|
88
|
-
*/
|
|
89
|
-
it('enlarges the circles when the marks are too small to key legibly', () => {
|
|
90
|
-
// Both the mark default ([2, 10]) and an explicit override ([2, 8]) -- the
|
|
91
|
-
// beeswarm fixture sets the latter, and checking only the default missed it.
|
|
92
|
-
const explicitRange: ChartSpec = {
|
|
93
|
-
...swarmSpec,
|
|
94
|
-
encoding: {
|
|
95
|
-
...swarmSpec.encoding,
|
|
96
|
-
size: { field: 'pop', type: 'quantitative', scale: { range: [2, 8] } },
|
|
97
|
-
},
|
|
98
|
-
} as ChartSpec;
|
|
99
|
-
|
|
100
|
-
for (const spec of [swarmSpec, explicitRange]) {
|
|
101
|
-
const circles = sizeLegendOf(spec)?.circles ?? [];
|
|
102
|
-
expect(circles.length).toBeGreaterThanOrEqual(2);
|
|
103
|
-
|
|
104
|
-
const maxRadius = Math.max(...circles.map((c) => c.radius));
|
|
105
|
-
// Bigger than the beeswarm's own cap, and still ordered + distinguishable.
|
|
106
|
-
expect(maxRadius).toBeGreaterThan(10);
|
|
107
|
-
for (let i = 1; i < circles.length; i++) {
|
|
108
|
-
expect(circles[i].radius).toBeLessThan(circles[i - 1].radius);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('nests the circles on a shared baseline, largest first', () => {
|
|
114
|
-
const circles = sizeLegendOf(bubbles)?.circles ?? [];
|
|
115
|
-
expect(circles.length).toBeGreaterThan(1);
|
|
116
|
-
|
|
117
|
-
// Largest first.
|
|
118
|
-
for (let i = 1; i < circles.length; i++) {
|
|
119
|
-
expect(circles[i].radius).toBeLessThan(circles[i - 1].radius);
|
|
120
|
-
}
|
|
121
|
-
// Concentric: every circle shares a bottom edge (cy + r is constant).
|
|
122
|
-
const baselines = circles.map((c) => c.cy + c.radius);
|
|
123
|
-
for (const b of baselines) expect(b).toBeCloseTo(baselines[0], 5);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('reserves its own right-margin instead of overlapping the plot', () => {
|
|
127
|
-
const withSize = compileChart(bubbles, OPTIONS);
|
|
128
|
-
const withoutSize = compileChart(
|
|
129
|
-
{ ...bubbles, encoding: { ...bubbles.encoding, size: undefined } } as ChartSpec,
|
|
130
|
-
OPTIONS,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// The plot narrows to make room -- it does not stay put and get drawn over.
|
|
134
|
-
expect(withSize.area.width).toBeLessThan(withoutSize.area.width);
|
|
135
|
-
|
|
136
|
-
const legend = withSize.legends.find((l): l is SizeLegendLayout => l.type === 'size');
|
|
137
|
-
expect(legend).toBeDefined();
|
|
138
|
-
// And the legend sits outside the plot's right edge.
|
|
139
|
-
expect(legend?.bounds.x).toBeGreaterThanOrEqual(withSize.area.x + withSize.area.width);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Clearing the plot is not enough: the color legend is in that same right
|
|
144
|
-
* gutter. Both used to anchor to `chartArea.x + chartArea.width`, so the
|
|
145
|
-
* graduated circles drew straight through the swatch labels -- reserving the
|
|
146
|
-
* margin for both but placing them at the same origin. Caught on a rendered
|
|
147
|
-
* bubble chart, not by a unit test, which is why this one exists.
|
|
148
|
-
*/
|
|
149
|
-
it('stacks below the color legend in a shared right column, not on top of it', () => {
|
|
150
|
-
const layout = compileChart(bubbles, OPTIONS);
|
|
151
|
-
const size = layout.legends.find((l) => l.type === 'size');
|
|
152
|
-
const color = layout.legends.find((l) => (l.channel ?? 'color') === 'color');
|
|
153
|
-
expect(size).toBeDefined();
|
|
154
|
-
expect(color).toBeDefined();
|
|
155
|
-
if (!size || !color) return;
|
|
156
|
-
|
|
157
|
-
const overlaps =
|
|
158
|
-
size.bounds.x < color.bounds.x + color.bounds.width &&
|
|
159
|
-
color.bounds.x < size.bounds.x + size.bounds.width &&
|
|
160
|
-
size.bounds.y < color.bounds.y + color.bounds.height &&
|
|
161
|
-
color.bounds.y < size.bounds.y + size.bounds.height;
|
|
162
|
-
expect(overlaps).toBe(false);
|
|
163
|
-
|
|
164
|
-
// Stacked, not side by side: the size legend starts below the color
|
|
165
|
-
// legend's bottom edge, and the two share a left edge (one column).
|
|
166
|
-
expect(size.bounds.y).toBeGreaterThanOrEqual(color.bounds.y + color.bounds.height);
|
|
167
|
-
expect(size.bounds.x).toBe(color.bounds.x);
|
|
168
|
-
// ...and the column still fits inside the SVG, horizontally and vertically.
|
|
169
|
-
expect(size.bounds.x + size.bounds.width).toBeLessThanOrEqual(OPTIONS.width);
|
|
170
|
-
expect(size.bounds.y + size.bounds.height).toBeLessThanOrEqual(OPTIONS.height);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* The point of stacking. Side by side, the two legends each carved their own
|
|
175
|
-
* width out of the gutter (`color + size + 2*gap`) to draw two blocks that are
|
|
176
|
-
* each only ~70px tall -- a 934px bubble chart spent ~213px of canvas on a key
|
|
177
|
-
* that used a fraction of it. One shared column costs the wider of the two, and
|
|
178
|
-
* the plot keeps the difference.
|
|
179
|
-
*/
|
|
180
|
-
it('gives the plot back the width the second gutter column used to cost', () => {
|
|
181
|
-
const withBoth = compileChart(bubbles, OPTIONS);
|
|
182
|
-
const colorOnly = compileChart(
|
|
183
|
-
{ ...bubbles, encoding: { ...bubbles.encoding, size: undefined } } as ChartSpec,
|
|
184
|
-
OPTIONS,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const size = withBoth.legends.find((l): l is SizeLegendLayout => l.type === 'size');
|
|
188
|
-
const color = withBoth.legends.find((l) => (l.channel ?? 'color') === 'color');
|
|
189
|
-
expect(size).toBeDefined();
|
|
190
|
-
expect(color).toBeDefined();
|
|
191
|
-
if (!size || !color) return;
|
|
192
|
-
|
|
193
|
-
// Adding a size legend must not cost the plot a second column. It may still
|
|
194
|
-
// narrow the plot (the size block can be wider than the color block), but by
|
|
195
|
-
// at most the difference between them -- never by the size block's full width.
|
|
196
|
-
const lostWidth = colorOnly.area.width - withBoth.area.width;
|
|
197
|
-
expect(lostWidth).toBeLessThan(size.bounds.width);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The size scale clamps, so with an explicit domain every datum past
|
|
202
|
-
* `domain[1]` renders at max radius. Keying the data extent would label the
|
|
203
|
-
* biggest circle with a value it doesn't actually correspond to.
|
|
204
|
-
*/
|
|
205
|
-
it('keys the scale domain, not the data extent', () => {
|
|
206
|
-
const capped: ChartSpec = {
|
|
207
|
-
...bubbles,
|
|
208
|
-
encoding: {
|
|
209
|
-
...bubbles.encoding,
|
|
210
|
-
size: { field: 'pop', type: 'quantitative', scale: { domain: [0, 100_000_000] } },
|
|
211
|
-
},
|
|
212
|
-
} as ChartSpec;
|
|
213
|
-
|
|
214
|
-
const circles = sizeLegendOf(capped)?.circles ?? [];
|
|
215
|
-
expect(circles.length).toBeGreaterThan(0);
|
|
216
|
-
// Nothing keyed above the domain ceiling, even though a datum hits 1.4B.
|
|
217
|
-
for (const c of circles) {
|
|
218
|
-
expect(c.value).toBeLessThanOrEqual(100_000_000);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('drops out when the size domain is degenerate (every value identical)', () => {
|
|
223
|
-
const flat: ChartSpec = {
|
|
224
|
-
...bubbles,
|
|
225
|
-
data: bubbles.data.map((d) => ({ ...(d as object), pop: 5_000_000 })),
|
|
226
|
-
} as ChartSpec;
|
|
227
|
-
// No magnitude is encoded, so there is nothing to key.
|
|
228
|
-
expect(sizeLegendOf(flat)).toBeUndefined();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('is suppressed by legend: { show: false }', () => {
|
|
232
|
-
expect(sizeLegendOf({ ...bubbles, legend: { show: false } })).toBeUndefined();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Font size is not a keyed channel. No publication legends it, and a stack of
|
|
237
|
-
* type at 12/24/48px reads as a font-sample sheet rather than a key.
|
|
238
|
-
*/
|
|
239
|
-
it('is not emitted for a text mark, whose size is emphasis rather than a key', () => {
|
|
240
|
-
const textSpec: ChartSpec = {
|
|
241
|
-
mark: { type: 'text' },
|
|
242
|
-
data: [...(bubbles.data as object[])] as ChartSpec['data'],
|
|
243
|
-
encoding: {
|
|
244
|
-
x: { field: 'gdp', type: 'quantitative' },
|
|
245
|
-
y: { field: 'life', type: 'quantitative' },
|
|
246
|
-
text: { field: 'region', type: 'nominal' },
|
|
247
|
-
size: { field: 'pop', type: 'quantitative' },
|
|
248
|
-
},
|
|
249
|
-
} as ChartSpec;
|
|
250
|
-
expect(sizeLegendOf(textSpec)).toBeUndefined();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('rounds the keyed values instead of showing raw data extremes', () => {
|
|
254
|
-
const circles = sizeLegendOf(bubbles)?.circles ?? [];
|
|
255
|
-
// 1.4B is the domain top and is always keyed; the interior values step down
|
|
256
|
-
// through the domain and nice-round, so no label is a long unrounded number.
|
|
257
|
-
const labels = circles.map((c) => c.label);
|
|
258
|
-
expect(labels.length).toBeGreaterThan(1);
|
|
259
|
-
expect(labels.some((l) => /^\d+(\.\d)?[KMB]?$/.test(l))).toBe(true);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
/** CIRCLE_COUNT is 3. An unconditional "always key the top" prepend made it 4. */
|
|
263
|
-
it('never keys more than three circles', () => {
|
|
264
|
-
for (const domain of [
|
|
265
|
-
[0, 1_400_000_000],
|
|
266
|
-
[0.5, 3],
|
|
267
|
-
[0, 6],
|
|
268
|
-
[1, 1000],
|
|
269
|
-
] as [number, number][]) {
|
|
270
|
-
const spec: ChartSpec = {
|
|
271
|
-
...bubbles,
|
|
272
|
-
encoding: {
|
|
273
|
-
...bubbles.encoding,
|
|
274
|
-
size: { field: 'pop', type: 'quantitative', scale: { domain } },
|
|
275
|
-
},
|
|
276
|
-
} as ChartSpec;
|
|
277
|
-
const circles = sizeLegendOf(spec)?.circles ?? [];
|
|
278
|
-
expect(circles.length).toBeLessThanOrEqual(3);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Stepping down from the top by halves collapsed on a narrow domain: for
|
|
284
|
-
* [90, 100], both 50 and 25 fall below the floor and get dropped, leaving one
|
|
285
|
-
* circle -- which keys nothing a reader didn't already assume.
|
|
286
|
-
*/
|
|
287
|
-
it('still keys a graduated scale on a narrow domain', () => {
|
|
288
|
-
for (const domain of [
|
|
289
|
-
[90, 100],
|
|
290
|
-
[7, 8],
|
|
291
|
-
] as [number, number][]) {
|
|
292
|
-
const spec: ChartSpec = {
|
|
293
|
-
...bubbles,
|
|
294
|
-
encoding: {
|
|
295
|
-
...bubbles.encoding,
|
|
296
|
-
size: { field: 'pop', type: 'quantitative', scale: { domain } },
|
|
297
|
-
},
|
|
298
|
-
} as ChartSpec;
|
|
299
|
-
const circles = sizeLegendOf(spec)?.circles ?? [];
|
|
300
|
-
expect(circles.length).toBeGreaterThanOrEqual(2);
|
|
301
|
-
// Strictly descending, so the circles are actually distinguishable.
|
|
302
|
-
for (let i = 1; i < circles.length; i++) {
|
|
303
|
-
expect(circles[i].radius).toBeLessThan(circles[i - 1].radius);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
/** Circle *area* encodes magnitude; a negative magnitude has no area to encode. */
|
|
309
|
-
it('is not emitted for a negative size domain', () => {
|
|
310
|
-
const spec: ChartSpec = {
|
|
311
|
-
...bubbles,
|
|
312
|
-
encoding: {
|
|
313
|
-
...bubbles.encoding,
|
|
314
|
-
size: { field: 'pop', type: 'quantitative', scale: { domain: [-100, -10] } },
|
|
315
|
-
},
|
|
316
|
-
} as ChartSpec;
|
|
317
|
-
expect(sizeLegendOf(spec)).toBeUndefined();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
/** The block is a fixed ~90px and can't reflow; on a phone it eats the plot. */
|
|
321
|
-
it('is suppressed below the compact width', () => {
|
|
322
|
-
expect(sizeLegendOf(bubbles, { width: 380, height: 300 })).toBeUndefined();
|
|
323
|
-
expect(sizeLegendOf(bubbles, { width: 800, height: 500 })).toBeDefined();
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
/** KEYED_SIZE_MARKS covers point AND beeswarm, but only point was exercised. */
|
|
327
|
-
it('is emitted for a beeswarm, whose size channel is also keyed', () => {
|
|
328
|
-
const legend = sizeLegendOf(swarmSpec);
|
|
329
|
-
expect(legend).toBeDefined();
|
|
330
|
-
expect((legend?.circles.length ?? 0) >= 2).toBe(true);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* A beeswarm's size range is [2, 10], so the whole circle stack is 20px tall.
|
|
335
|
-
* Labels pinned to each circle's top edge piled on top of each other -- three
|
|
336
|
-
* unreadable lines of text. Caught by the visual baseline, not by a unit test.
|
|
337
|
-
*/
|
|
338
|
-
it('keeps labels legible when the circles are too small to space them', () => {
|
|
339
|
-
for (const spec of [swarmSpec, bubbles]) {
|
|
340
|
-
const circles = sizeLegendOf(spec)?.circles ?? [];
|
|
341
|
-
expect(circles.length).toBeGreaterThanOrEqual(2);
|
|
342
|
-
// Labels run top-down and never collide, whatever the radii.
|
|
343
|
-
for (let i = 1; i < circles.length; i++) {
|
|
344
|
-
expect(circles[i].labelY - circles[i - 1].labelY).toBeGreaterThanOrEqual(11);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
});
|