@opendata-ai/openchart-engine 8.0.0-rc.10 → 8.0.0-rc.11

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