@opendata-ai/openchart-engine 8.0.0-rc.2 → 8.0.0-rc.20

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.
Files changed (271) hide show
  1. package/dist/index.d.ts +140 -2
  2. package/dist/index.js +1403 -210
  3. package/dist/index.js.map +1 -1
  4. package/package.json +3 -4
  5. package/src/__test-fixtures__/specs.ts +0 -182
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
  7. package/src/__tests__/annotation-behavior.test.ts +0 -721
  8. package/src/__tests__/axes.test.ts +0 -1579
  9. package/src/__tests__/compile-animation.test.ts +0 -161
  10. package/src/__tests__/compile-chart.test.ts +0 -1303
  11. package/src/__tests__/compile-footnotes.test.ts +0 -277
  12. package/src/__tests__/compile-layer.test.ts +0 -969
  13. package/src/__tests__/compile-snapshot.test.ts +0 -156
  14. package/src/__tests__/compound-labels.test.ts +0 -147
  15. package/src/__tests__/dimensions.test.ts +0 -918
  16. package/src/__tests__/encoding-sugar.test.ts +0 -390
  17. package/src/__tests__/facet.test.ts +0 -215
  18. package/src/__tests__/helpers/invariants.ts +0 -299
  19. package/src/__tests__/known-bugs.test.ts +0 -566
  20. package/src/__tests__/layout-fuzz.test.ts +0 -358
  21. package/src/__tests__/layout-invariants.test.ts +0 -535
  22. package/src/__tests__/legend.test.ts +0 -721
  23. package/src/__tests__/mark-keys.test.ts +0 -294
  24. package/src/__tests__/ranking-recipes.test.ts +0 -260
  25. package/src/__tests__/rotation.test.ts +0 -83
  26. package/src/__tests__/scales.test.ts +0 -323
  27. package/src/__tests__/series-search.test.ts +0 -92
  28. package/src/__tests__/spec-sugar.test.ts +0 -801
  29. package/src/__tests__/you-draw-it.test.ts +0 -160
  30. package/src/annotations/__tests__/compute.test.ts +0 -1672
  31. package/src/annotations/__tests__/geometry.test.ts +0 -66
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
  33. package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
  34. package/src/annotations/__tests__/placement.test.ts +0 -316
  35. package/src/annotations/__tests__/resolve-text.test.ts +0 -788
  36. package/src/annotations/__tests__/rich-text.test.ts +0 -91
  37. package/src/annotations/__tests__/thinning.test.ts +0 -285
  38. package/src/annotations/collisions.ts +0 -326
  39. package/src/annotations/compute.ts +0 -333
  40. package/src/annotations/constants.ts +0 -162
  41. package/src/annotations/geometry.ts +0 -370
  42. package/src/annotations/move.ts +0 -135
  43. package/src/annotations/placement.ts +0 -636
  44. package/src/annotations/position.ts +0 -131
  45. package/src/annotations/resolve-range.ts +0 -117
  46. package/src/annotations/resolve-refline.ts +0 -157
  47. package/src/annotations/resolve-text.ts +0 -539
  48. package/src/annotations/rich-text.ts +0 -65
  49. package/src/annotations/thinning.ts +0 -190
  50. package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
  51. package/src/barlist/compile-barlist.ts +0 -391
  52. package/src/barlist/types.ts +0 -28
  53. package/src/charts/__tests__/post-process.test.ts +0 -237
  54. package/src/charts/__tests__/registry.test.ts +0 -116
  55. package/src/charts/__tests__/utils.test.ts +0 -195
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
  57. package/src/charts/_shared/density-filter.ts +0 -26
  58. package/src/charts/_shared/format-label-value.ts +0 -16
  59. package/src/charts/bar/__tests__/compute.test.ts +0 -934
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
  61. package/src/charts/bar/__tests__/labels.test.ts +0 -292
  62. package/src/charts/bar/compute.ts +0 -539
  63. package/src/charts/bar/index.ts +0 -60
  64. package/src/charts/bar/labels.ts +0 -311
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
  67. package/src/charts/beeswarm/compute.ts +0 -203
  68. package/src/charts/beeswarm/dodge.ts +0 -155
  69. package/src/charts/beeswarm/index.ts +0 -32
  70. package/src/charts/builtin.ts +0 -75
  71. package/src/charts/calendar/__tests__/compute.test.ts +0 -417
  72. package/src/charts/calendar/compute.ts +0 -319
  73. package/src/charts/calendar/index.ts +0 -32
  74. package/src/charts/column/__tests__/compute.test.ts +0 -540
  75. package/src/charts/column/__tests__/labels.test.ts +0 -179
  76. package/src/charts/column/compute.ts +0 -493
  77. package/src/charts/column/index.ts +0 -53
  78. package/src/charts/column/labels.ts +0 -229
  79. package/src/charts/dot/__tests__/compute.test.ts +0 -380
  80. package/src/charts/dot/__tests__/labels.test.ts +0 -98
  81. package/src/charts/dot/compute.ts +0 -285
  82. package/src/charts/dot/index.ts +0 -62
  83. package/src/charts/dot/labels.ts +0 -121
  84. package/src/charts/line/__tests__/compute.test.ts +0 -1194
  85. package/src/charts/line/__tests__/curves.test.ts +0 -51
  86. package/src/charts/line/__tests__/labels.test.ts +0 -94
  87. package/src/charts/line/__tests__/mark-options.test.ts +0 -175
  88. package/src/charts/line/area.ts +0 -484
  89. package/src/charts/line/compute.ts +0 -356
  90. package/src/charts/line/curves.ts +0 -85
  91. package/src/charts/line/index.ts +0 -176
  92. package/src/charts/line/labels.ts +0 -177
  93. package/src/charts/parliament/__tests__/compute.test.ts +0 -411
  94. package/src/charts/parliament/compute.ts +0 -313
  95. package/src/charts/parliament/index.ts +0 -32
  96. package/src/charts/pie/__tests__/compute.test.ts +0 -384
  97. package/src/charts/pie/__tests__/labels.test.ts +0 -132
  98. package/src/charts/pie/compute.ts +0 -324
  99. package/src/charts/pie/index.ts +0 -49
  100. package/src/charts/pie/labels.ts +0 -143
  101. package/src/charts/post-process.ts +0 -229
  102. package/src/charts/range/__tests__/compute.test.ts +0 -486
  103. package/src/charts/range/compute.ts +0 -273
  104. package/src/charts/range/index.ts +0 -82
  105. package/src/charts/range/labels.ts +0 -169
  106. package/src/charts/rect/__tests__/compute.test.ts +0 -120
  107. package/src/charts/rect/index.ts +0 -127
  108. package/src/charts/registry.ts +0 -72
  109. package/src/charts/rule/index.ts +0 -139
  110. package/src/charts/scatter/__tests__/compute.test.ts +0 -488
  111. package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
  112. package/src/charts/scatter/compute.ts +0 -192
  113. package/src/charts/scatter/index.ts +0 -46
  114. package/src/charts/scatter/trendline.ts +0 -100
  115. package/src/charts/text/__tests__/compute.test.ts +0 -307
  116. package/src/charts/text/index.ts +0 -141
  117. package/src/charts/tick/__tests__/compute.test.ts +0 -75
  118. package/src/charts/tick/index.ts +0 -99
  119. package/src/charts/utils.ts +0 -170
  120. package/src/charts/waffle/__tests__/compute.test.ts +0 -305
  121. package/src/charts/waffle/compute.ts +0 -197
  122. package/src/charts/waffle/index.ts +0 -31
  123. package/src/compile/__tests__/color-scale-range.test.ts +0 -158
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
  125. package/src/compile/__tests__/data-clip.test.ts +0 -127
  126. package/src/compile/__tests__/fill-patterns.test.ts +0 -154
  127. package/src/compile/__tests__/size-scale.test.ts +0 -108
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
  129. package/src/compile/color-scale-range.ts +0 -101
  130. package/src/compile/data-clip.ts +0 -48
  131. package/src/compile/fill-patterns.ts +0 -74
  132. package/src/compile/layer.ts +0 -626
  133. package/src/compile/size-scale.ts +0 -139
  134. package/src/compile/spec-sugar.ts +0 -758
  135. package/src/compile/watermark-obstacle.ts +0 -54
  136. package/src/compile/you-draw-it.ts +0 -123
  137. package/src/compile.ts +0 -1450
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
  139. package/src/compiler/__tests__/animation.test.ts +0 -177
  140. package/src/compiler/__tests__/compile.test.ts +0 -87
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
  160. package/src/compiler/__tests__/keys.test.ts +0 -83
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
  162. package/src/compiler/__tests__/normalize.test.ts +0 -399
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -86
  165. package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +0 -89
  167. package/src/compiler/__tests__/validate.test.ts +0 -629
  168. package/src/compiler/__tests__/vl-parity.test.ts +0 -68
  169. package/src/compiler/a11y-warnings.ts +0 -110
  170. package/src/compiler/animation.ts +0 -142
  171. package/src/compiler/index.ts +0 -47
  172. package/src/compiler/keys.ts +0 -55
  173. package/src/compiler/normalize.ts +0 -733
  174. package/src/compiler/sparkline-defaults.ts +0 -138
  175. package/src/compiler/types.ts +0 -241
  176. package/src/compiler/validate.ts +0 -1810
  177. package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
  178. package/src/endpoint-labels/compute.ts +0 -621
  179. package/src/endpoint-labels/constants.ts +0 -54
  180. package/src/endpoint-labels/format.ts +0 -60
  181. package/src/endpoint-labels/predict.ts +0 -139
  182. package/src/format/__tests__/field-format.test.ts +0 -164
  183. package/src/format/field-format.ts +0 -27
  184. package/src/geo/__tests__/compile-map.test.ts +0 -621
  185. package/src/geo/compile-map.ts +0 -856
  186. package/src/geo/join.ts +0 -115
  187. package/src/geo/projections.ts +0 -45
  188. package/src/geo/types.ts +0 -24
  189. package/src/graphs/__tests__/community.test.ts +0 -228
  190. package/src/graphs/__tests__/compile-graph.test.ts +0 -396
  191. package/src/graphs/__tests__/encoding.test.ts +0 -415
  192. package/src/graphs/community.ts +0 -92
  193. package/src/graphs/compile-graph.ts +0 -329
  194. package/src/graphs/encoding.ts +0 -356
  195. package/src/graphs/types.ts +0 -106
  196. package/src/index.ts +0 -177
  197. package/src/layout/__tests__/metrics.test.ts +0 -97
  198. package/src/layout/__tests__/scales-highlight.test.ts +0 -192
  199. package/src/layout/axes/rotation.ts +0 -152
  200. package/src/layout/axes/thinning.ts +0 -98
  201. package/src/layout/axes/ticks.ts +0 -409
  202. package/src/layout/axes.ts +0 -757
  203. package/src/layout/dimensions.ts +0 -912
  204. package/src/layout/facet.ts +0 -91
  205. package/src/layout/gridlines.ts +0 -84
  206. package/src/layout/metrics.ts +0 -132
  207. package/src/layout/plan.ts +0 -631
  208. package/src/layout/scales.ts +0 -1053
  209. package/src/layout/shared.ts +0 -44
  210. package/src/legend/__tests__/continuous.test.ts +0 -318
  211. package/src/legend/__tests__/redundancy.test.ts +0 -108
  212. package/src/legend/__tests__/size.test.ts +0 -348
  213. package/src/legend/__tests__/suppression.test.ts +0 -382
  214. package/src/legend/compute.ts +0 -747
  215. package/src/legend/continuous.ts +0 -327
  216. package/src/legend/size.ts +0 -407
  217. package/src/legend/suppression.ts +0 -226
  218. package/src/legend/wrap.ts +0 -117
  219. package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
  220. package/src/sankey/__tests__/layout.test.ts +0 -165
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
  222. package/src/sankey/__tests__/node-sort.test.ts +0 -45
  223. package/src/sankey/compile-sankey.ts +0 -706
  224. package/src/sankey/layout.ts +0 -184
  225. package/src/sankey/types.ts +0 -41
  226. package/src/tables/__tests__/bar-column.test.ts +0 -147
  227. package/src/tables/__tests__/category-colors.test.ts +0 -230
  228. package/src/tables/__tests__/compile-table.test.ts +0 -236
  229. package/src/tables/__tests__/format-cells.test.ts +0 -143
  230. package/src/tables/__tests__/heatmap.test.ts +0 -148
  231. package/src/tables/__tests__/pagination.test.ts +0 -78
  232. package/src/tables/__tests__/search.test.ts +0 -94
  233. package/src/tables/__tests__/sort.test.ts +0 -107
  234. package/src/tables/__tests__/sparkline.test.ts +0 -122
  235. package/src/tables/bar-column.ts +0 -94
  236. package/src/tables/category-colors.ts +0 -78
  237. package/src/tables/compile-table.ts +0 -427
  238. package/src/tables/format-cells.ts +0 -115
  239. package/src/tables/heatmap.ts +0 -141
  240. package/src/tables/pagination.ts +0 -46
  241. package/src/tables/search.ts +0 -66
  242. package/src/tables/sort.ts +0 -69
  243. package/src/tables/sparkline.ts +0 -113
  244. package/src/tables/utils.ts +0 -16
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
  246. package/src/tilemap/compile-tilemap.ts +0 -755
  247. package/src/tilemap/layout.ts +0 -172
  248. package/src/tilemap/types.ts +0 -33
  249. package/src/tooltips/__tests__/compute.test.ts +0 -582
  250. package/src/tooltips/compute.ts +0 -700
  251. package/src/transforms/__tests__/aggregate.test.ts +0 -159
  252. package/src/transforms/__tests__/bin.test.ts +0 -88
  253. package/src/transforms/__tests__/calculate.test.ts +0 -146
  254. package/src/transforms/__tests__/conditional.test.ts +0 -127
  255. package/src/transforms/__tests__/filter-relative.test.ts +0 -202
  256. package/src/transforms/__tests__/filter.test.ts +0 -59
  257. package/src/transforms/__tests__/fold.test.ts +0 -79
  258. package/src/transforms/__tests__/index.test.ts +0 -93
  259. package/src/transforms/__tests__/predicates.test.ts +0 -193
  260. package/src/transforms/__tests__/timeunit.test.ts +0 -129
  261. package/src/transforms/__tests__/window.test.ts +0 -286
  262. package/src/transforms/aggregate.ts +0 -131
  263. package/src/transforms/bin.ts +0 -87
  264. package/src/transforms/calculate.ts +0 -60
  265. package/src/transforms/conditional.ts +0 -46
  266. package/src/transforms/filter.ts +0 -122
  267. package/src/transforms/fold.ts +0 -49
  268. package/src/transforms/index.ts +0 -60
  269. package/src/transforms/predicates.ts +0 -123
  270. package/src/transforms/timeunit.ts +0 -88
  271. 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
- });