@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,91 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { computeTextBlockBounds, heuristicMeasure } from '../geometry';
3
- import { parseAnnotationSpans } from '../rich-text';
4
-
5
- describe('parseAnnotationSpans', () => {
6
- it('returns a single regular span for plain text', () => {
7
- expect(parseAnnotationSpans('Inflation cooled')).toEqual([
8
- { text: 'Inflation cooled', bold: false },
9
- ]);
10
- });
11
-
12
- it('splits a single bold span out of the surrounding text', () => {
13
- expect(parseAnnotationSpans('Inflation peaked at **8.5%** in June')).toEqual([
14
- { text: 'Inflation peaked at ', bold: false },
15
- { text: '8.5%', bold: true },
16
- { text: ' in June', bold: false },
17
- ]);
18
- });
19
-
20
- it('handles multiple bold spans on one line', () => {
21
- expect(parseAnnotationSpans('In **California,** ballots arrive by **Nov. 20**')).toEqual([
22
- { text: 'In ', bold: false },
23
- { text: 'California,', bold: true },
24
- { text: ' ballots arrive by ', bold: false },
25
- { text: 'Nov. 20', bold: true },
26
- ]);
27
- });
28
-
29
- it('bolds a span at the start of the line', () => {
30
- expect(parseAnnotationSpans('**Feb. 25** peak')).toEqual([
31
- { text: 'Feb. 25', bold: true },
32
- { text: ' peak', bold: false },
33
- ]);
34
- });
35
-
36
- it('bolds a span at the end of the line', () => {
37
- expect(parseAnnotationSpans('Peak on **Feb. 25**')).toEqual([
38
- { text: 'Peak on ', bold: false },
39
- { text: 'Feb. 25', bold: true },
40
- ]);
41
- });
42
-
43
- it('renders an unmatched delimiter literally', () => {
44
- expect(parseAnnotationSpans('Up **50% since 2020')).toEqual([
45
- { text: 'Up **50% since 2020', bold: false },
46
- ]);
47
- });
48
-
49
- it('renders an empty delimiter pair literally', () => {
50
- expect(parseAnnotationSpans('Nothing **** here')).toEqual([
51
- { text: 'Nothing **** here', bold: false },
52
- ]);
53
- });
54
-
55
- it('never returns an empty span list', () => {
56
- expect(parseAnnotationSpans('')).toEqual([{ text: '', bold: false }]);
57
- });
58
- });
59
-
60
- describe('computeTextBlockBounds with rich text', () => {
61
- const style = {
62
- fontSize: 13,
63
- fontWeight: 400,
64
- lineHeight: 1.3,
65
- textAnchor: 'start' as const,
66
- };
67
-
68
- it('measures a line with bold spans wider than the same line all-regular', () => {
69
- const bold = computeTextBlockBounds(0, 20, 'Inflation peaked at **8.5%**', style);
70
- const plain = computeTextBlockBounds(0, 20, 'Inflation peaked at 8.5%', style);
71
-
72
- expect(bold.width).toBeGreaterThan(plain.width);
73
- });
74
-
75
- it('does not count the ** markers in the measured width', () => {
76
- // The markers are syntax, not glyphs. A naive raw-string measure would make
77
- // the bold version wider by four asterisks at weight 400 — this asserts the
78
- // widening comes from the weight, not the delimiters.
79
- const withMarkers = computeTextBlockBounds(0, 20, '**8.5%**', style, heuristicMeasure);
80
- const boldOnly = heuristicMeasure('8.5%', { fontSize: 13, fontWeight: 700 });
81
-
82
- expect(withMarkers.width).toBeCloseTo(boldOnly, 5);
83
- });
84
-
85
- it('measures multi-line rich text by its widest line', () => {
86
- const bounds = computeTextBlockBounds(0, 20, 'short\n**a much longer bold line**', style);
87
- const longestAlone = computeTextBlockBounds(0, 20, '**a much longer bold line**', style);
88
-
89
- expect(bounds.width).toBeCloseTo(longestAlone.width, 5);
90
- });
91
- });
@@ -1,285 +0,0 @@
1
- import type { Annotation, ResolvedAnnotation, TextAnnotation } from '@opendata-ai/openchart-core';
2
- import { describe, expect, it } from 'vitest';
3
- import { thinAnnotations } from '../thinning';
4
-
5
- const measure = (text: string, font: { fontSize: number }) => text.length * font.fontSize * 0.6;
6
-
7
- function makeResolved(
8
- x: number,
9
- y: number,
10
- text: string,
11
- overrides?: Partial<ResolvedAnnotation>,
12
- ): ResolvedAnnotation {
13
- return {
14
- type: 'text',
15
- label: {
16
- x,
17
- y,
18
- text,
19
- visible: true,
20
- style: { fontSize: 12, fontWeight: 400, textAnchor: 'start', lineHeight: 1.3 },
21
- },
22
- ...overrides,
23
- } as ResolvedAnnotation;
24
- }
25
-
26
- function makeSpec(text: string, overrides?: Partial<TextAnnotation>): TextAnnotation {
27
- return {
28
- type: 'text',
29
- x: 0,
30
- y: 0,
31
- text,
32
- ...overrides,
33
- } as TextAnnotation;
34
- }
35
-
36
- describe('thinAnnotations', () => {
37
- it('returns unchanged when 0 or 1 annotations', () => {
38
- const single = [makeResolved(100, 100, 'A')];
39
- const result = thinAnnotations(single, [makeSpec('A')], measure);
40
- expect(result.footnotes).toHaveLength(0);
41
- expect(result.annotations).toBe(single);
42
- });
43
-
44
- it('keeps non-overlapping annotations in place', () => {
45
- const annotations = [makeResolved(0, 0, 'First'), makeResolved(500, 500, 'Second')];
46
- const specs: Annotation[] = [makeSpec('First'), makeSpec('Second')];
47
- const result = thinAnnotations(annotations, specs, measure);
48
- expect(result.footnotes).toHaveLength(0);
49
- expect(result.annotations[0].footnoteIndex).toBeUndefined();
50
- expect(result.annotations[1].footnoteIndex).toBeUndefined();
51
- });
52
-
53
- it('demotes overlapping annotations to footnotes', () => {
54
- const annotations = [
55
- makeResolved(100, 100, 'First label'),
56
- makeResolved(105, 100, 'Second label'),
57
- ];
58
- const specs: Annotation[] = [makeSpec('First label'), makeSpec('Second label')];
59
- const result = thinAnnotations(annotations, specs, measure);
60
- expect(result.footnotes).toHaveLength(1);
61
- expect(result.footnotes[0]).toEqual({ index: 1, text: 'Second label' });
62
- expect(result.annotations[1].footnoteIndex).toBe(1);
63
- expect(result.annotations[0].footnoteIndex).toBeUndefined();
64
- });
65
-
66
- it('respects priority ordering (lower priority kept first)', () => {
67
- const annotations = [
68
- makeResolved(100, 100, 'Low priority'),
69
- makeResolved(105, 100, 'High priority'),
70
- ];
71
- const specs: Annotation[] = [
72
- makeSpec('Low priority', { priority: 10 }),
73
- makeSpec('High priority', { priority: 1 }),
74
- ];
75
- const result = thinAnnotations(annotations, specs, measure);
76
- expect(result.footnotes).toHaveLength(1);
77
- expect(result.footnotes[0].text).toBe('Low priority');
78
- expect(result.annotations[1].footnoteIndex).toBeUndefined();
79
- expect(result.annotations[0].footnoteIndex).toBe(1);
80
- });
81
-
82
- it('pins annotations with responsive: false', () => {
83
- const annotations = [makeResolved(100, 100, 'Pinned'), makeResolved(105, 100, 'Candidate')];
84
- const specs: Annotation[] = [makeSpec('Pinned', { responsive: false }), makeSpec('Candidate')];
85
- const result = thinAnnotations(annotations, specs, measure);
86
- expect(result.footnotes).toHaveLength(1);
87
- expect(result.footnotes[0].text).toBe('Candidate');
88
- expect(result.annotations[0].footnoteIndex).toBeUndefined();
89
- });
90
-
91
- it('does not thin non-text annotations', () => {
92
- const annotations: ResolvedAnnotation[] = [
93
- {
94
- type: 'refline',
95
- line: { start: { x: 0, y: 0 }, end: { x: 100, y: 0 } },
96
- } as ResolvedAnnotation,
97
- makeResolved(100, 100, 'Text'),
98
- ];
99
- const specs: Annotation[] = [{ type: 'refline', y: 0 }, makeSpec('Text')];
100
- const result = thinAnnotations(annotations, specs, measure);
101
- expect(result.footnotes).toHaveLength(0);
102
- });
103
-
104
- it('assigns sequential footnote indices', () => {
105
- const annotations = [
106
- makeResolved(100, 100, 'Keep this'),
107
- makeResolved(105, 100, 'Demote 1'),
108
- makeResolved(110, 100, 'Demote 2'),
109
- ];
110
- const specs: Annotation[] = [makeSpec('Keep this'), makeSpec('Demote 1'), makeSpec('Demote 2')];
111
- const result = thinAnnotations(annotations, specs, measure);
112
- expect(result.footnotes).toHaveLength(2);
113
- expect(result.footnotes[0].index).toBe(1);
114
- expect(result.footnotes[1].index).toBe(2);
115
- });
116
-
117
- it('maintains correct index alignment with mixed annotation types', () => {
118
- const annotations: ResolvedAnnotation[] = [
119
- {
120
- type: 'refline',
121
- line: { start: { x: 0, y: 0 }, end: { x: 100, y: 0 } },
122
- } as ResolvedAnnotation,
123
- makeResolved(100, 100, 'First text'),
124
- makeResolved(105, 100, 'Second text'),
125
- ];
126
- const specs: Annotation[] = [
127
- { type: 'refline', y: 0 },
128
- makeSpec('First text', { priority: 1 }),
129
- makeSpec('Second text', { priority: 10 }),
130
- ];
131
- const result = thinAnnotations(annotations, specs, measure);
132
- expect(result.footnotes).toHaveLength(1);
133
- expect(result.footnotes[0].text).toBe('Second text');
134
- expect(result.annotations[1].footnoteIndex).toBeUndefined();
135
- expect(result.annotations[2].footnoteIndex).toBe(1);
136
- });
137
-
138
- describe('plot-area containment', () => {
139
- // Label `y` is the text baseline, so bounds extend upward from it: a label
140
- // at y=40 with a 12px font spans roughly y=28..44. Keep test baselines far
141
- // enough from the top edge that a fitting label really does fit.
142
- const plotArea = { x: 0, y: 0, width: 120, height: 200 };
143
-
144
- it('demotes a label that overflows the plot even when nothing collides', () => {
145
- // Stacked at different y, so the two never pairwise-collide. The second
146
- // runs off the right edge and must still demote.
147
- const annotations = [
148
- makeResolved(0, 40, 'Fits'),
149
- makeResolved(60, 120, 'Way too long to fit here'),
150
- ];
151
- const specs: Annotation[] = [makeSpec('Fits'), makeSpec('Way too long to fit here')];
152
- const result = thinAnnotations(annotations, specs, measure, plotArea);
153
- expect(result.footnotes).toHaveLength(1);
154
- expect(result.footnotes[0].text).toBe('Way too long to fit here');
155
- expect(result.annotations[0].footnoteIndex).toBeUndefined();
156
- expect(result.annotations[1].footnoteIndex).toBe(1);
157
- });
158
-
159
- it('keeps a label that fits inside the plot', () => {
160
- const annotations = [makeResolved(0, 40, 'A'), makeResolved(0, 120, 'B')];
161
- const specs: Annotation[] = [makeSpec('A'), makeSpec('B')];
162
- const result = thinAnnotations(annotations, specs, measure, plotArea);
163
- expect(result.footnotes).toHaveLength(0);
164
- });
165
-
166
- it('never demotes a pinned label, even when it overflows', () => {
167
- const annotations = [
168
- makeResolved(0, 40, 'Fits'),
169
- makeResolved(60, 120, 'Way too long to fit here'),
170
- ];
171
- const specs: Annotation[] = [
172
- makeSpec('Fits'),
173
- makeSpec('Way too long to fit here', { responsive: false }),
174
- ];
175
- const result = thinAnnotations(annotations, specs, measure, plotArea);
176
- expect(result.footnotes).toHaveLength(0);
177
- expect(result.annotations[1].footnoteIndex).toBeUndefined();
178
- });
179
-
180
- it('applies no containment constraint when plotArea is omitted', () => {
181
- const annotations = [
182
- makeResolved(0, 40, 'Fits'),
183
- makeResolved(60, 120, 'Way too long to fit here'),
184
- ];
185
- const specs: Annotation[] = [makeSpec('Fits'), makeSpec('Way too long to fit here')];
186
- const result = thinAnnotations(annotations, specs, measure);
187
- expect(result.footnotes).toHaveLength(0);
188
- });
189
-
190
- // A callout above a peak or below a trough lands in the margin, outside the
191
- // plot but inside the chart. Annotations render outside the clip path, so
192
- // that is legal placement and must stay inline. Fencing containment to the
193
- // plot demoted ordinary callouts to footnotes.
194
- //
195
- // The plot is inset inside the chart here (a real top margin exists), so
196
- // "above the plot" and "outside the chart" are genuinely different regions.
197
- // Roomy enough that the coverage budget stays out of the way — containment
198
- // is what's under test here.
199
- const insetPlot = { x: 40, y: 60, width: 700, height: 400 };
200
- const chart = { x: 0, y: 0, width: 780, height: 520 };
201
-
202
- it('keeps a label sitting in the margin above the plot', () => {
203
- // Baseline y=30: bounds span roughly y=18..34, above insetPlot.y (60) but
204
- // well inside the chart. Fenced to the plot this demotes; fenced to the
205
- // chart it stays inline.
206
- const annotations = [makeResolved(50, 120, 'Inside'), makeResolved(50, 30, 'Above the peak')];
207
- const specs: Annotation[] = [makeSpec('Inside'), makeSpec('Above the peak')];
208
-
209
- const result = thinAnnotations(annotations, specs, measure, insetPlot, chart);
210
-
211
- expect(result.footnotes).toHaveLength(0);
212
- expect(result.annotations[1].footnoteIndex).toBeUndefined();
213
- });
214
-
215
- it('still demotes a label that escapes the chart entirely', () => {
216
- // x=740 in a 780-wide chart: the text runs past the right edge.
217
- const annotations = [
218
- makeResolved(50, 120, 'Inside'),
219
- makeResolved(740, 120, 'Off the right edge of the chart'),
220
- ];
221
- const specs: Annotation[] = [makeSpec('Inside'), makeSpec('Off the right edge of the chart')];
222
-
223
- const result = thinAnnotations(annotations, specs, measure, insetPlot, chart);
224
-
225
- expect(result.footnotes).toHaveLength(1);
226
- expect(result.annotations[1].footnoteIndex).toBe(1);
227
- });
228
- });
229
-
230
- describe('coverage budget', () => {
231
- // Four labels, each well clear of the others and of the plot edges, so
232
- // neither the overlap test nor the containment test fires. Only the
233
- // coverage budget can demote here — which is the point: at narrow widths
234
- // collision resolution spreads labels out and clamping tucks them back in,
235
- // leaving a crowded-but-legal layout that the other two rules cannot see.
236
- // Editorial-length copy, not toy strings: label area is what the budget
237
- // meters, and real callouts ("Recovery begins") are several times wider
238
- // than a five-letter placeholder.
239
- const TEXTS = ['Recovery begins', 'Strong rebound', 'Record territory', 'Pandemic low'];
240
- const spread = () => TEXTS.map((t, i) => makeResolved(5, 20 + i * 40, t));
241
- const specs: Annotation[] = TEXTS.map((t) => makeSpec(t));
242
-
243
- it('demotes the crowded tail once labels exceed the budget on a small plot', () => {
244
- const annotations = spread();
245
- const plotArea = { x: 0, y: 0, width: 160, height: 160 };
246
- const result = thinAnnotations(annotations, specs, measure, plotArea);
247
-
248
- // Nothing collides and everything is inside the plot, so a demotion here
249
- // is attributable to the budget alone.
250
- expect(result.footnotes.length).toBeGreaterThan(0);
251
-
252
- // Priority ties fall back to spec order, so the tail demotes and the head
253
- // stays inline.
254
- expect(result.annotations[0].footnoteIndex).toBeUndefined();
255
- expect(result.annotations[3].footnoteIndex).toBeDefined();
256
- });
257
-
258
- it('leaves every label inline when the plot has room', () => {
259
- const annotations = spread();
260
- const plotArea = { x: 0, y: 0, width: 900, height: 500 };
261
- const result = thinAnnotations(annotations, specs, measure, plotArea);
262
- expect(result.footnotes).toHaveLength(0);
263
- });
264
-
265
- it('does not let a pinned label consume the budget and evict candidates', () => {
266
- // The pinned label is enormous — on its own it would blow the budget. It
267
- // must not be charged against it, or the candidate that easily fits gets
268
- // demoted in its place.
269
- const annotations = [
270
- makeResolved(5, 20, 'A pinned label that is extremely long and wide'),
271
- makeResolved(5, 120, 'Tiny'),
272
- ];
273
- const pinnedSpecs: Annotation[] = [
274
- makeSpec('A pinned label that is extremely long and wide', { responsive: false }),
275
- makeSpec('Tiny'),
276
- ];
277
- const plotArea = { x: 0, y: 0, width: 400, height: 200 };
278
- const result = thinAnnotations(annotations, pinnedSpecs, measure, plotArea);
279
-
280
- expect(result.annotations[0].footnoteIndex).toBeUndefined();
281
- expect(result.annotations[1].footnoteIndex).toBeUndefined();
282
- expect(result.footnotes).toHaveLength(0);
283
- });
284
- });
285
- });