@opendata-ai/openchart-engine 8.0.0-rc.12 → 8.0.0-rc.3

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 +0 -7
  2. package/dist/index.js +161 -579
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  5. package/src/__test-fixtures__/specs.ts +182 -0
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +2339 -0
  7. package/src/__tests__/annotation-behavior.test.ts +721 -0
  8. package/src/__tests__/axes.test.ts +1579 -0
  9. package/src/__tests__/compile-animation.test.ts +161 -0
  10. package/src/__tests__/compile-chart.test.ts +1303 -0
  11. package/src/__tests__/compile-footnotes.test.ts +277 -0
  12. package/src/__tests__/compile-layer.test.ts +969 -0
  13. package/src/__tests__/compile-snapshot.test.ts +156 -0
  14. package/src/__tests__/compound-labels.test.ts +147 -0
  15. package/src/__tests__/dimensions.test.ts +918 -0
  16. package/src/__tests__/encoding-sugar.test.ts +390 -0
  17. package/src/__tests__/facet.test.ts +215 -0
  18. package/src/__tests__/helpers/invariants.ts +299 -0
  19. package/src/__tests__/known-bugs.test.ts +566 -0
  20. package/src/__tests__/layout-fuzz.test.ts +358 -0
  21. package/src/__tests__/layout-invariants.test.ts +535 -0
  22. package/src/__tests__/legend.test.ts +721 -0
  23. package/src/__tests__/mark-keys.test.ts +294 -0
  24. package/src/__tests__/ranking-recipes.test.ts +260 -0
  25. package/src/__tests__/rotation.test.ts +83 -0
  26. package/src/__tests__/scales.test.ts +323 -0
  27. package/src/__tests__/series-search.test.ts +92 -0
  28. package/src/__tests__/spec-sugar.test.ts +801 -0
  29. package/src/__tests__/you-draw-it.test.ts +160 -0
  30. package/src/annotations/__tests__/compute.test.ts +1672 -0
  31. package/src/annotations/__tests__/geometry.test.ts +66 -0
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +256 -0
  33. package/src/annotations/__tests__/placement-invariants.test.ts +345 -0
  34. package/src/annotations/__tests__/placement.test.ts +316 -0
  35. package/src/annotations/__tests__/resolve-text.test.ts +788 -0
  36. package/src/annotations/__tests__/rich-text.test.ts +91 -0
  37. package/src/annotations/__tests__/thinning.test.ts +285 -0
  38. package/src/annotations/collisions.ts +326 -0
  39. package/src/annotations/compute.ts +333 -0
  40. package/src/annotations/constants.ts +162 -0
  41. package/src/annotations/geometry.ts +370 -0
  42. package/src/annotations/move.ts +135 -0
  43. package/src/annotations/placement.ts +636 -0
  44. package/src/annotations/position.ts +131 -0
  45. package/src/annotations/resolve-range.ts +117 -0
  46. package/src/annotations/resolve-refline.ts +157 -0
  47. package/src/annotations/resolve-text.ts +539 -0
  48. package/src/annotations/rich-text.ts +65 -0
  49. package/src/annotations/thinning.ts +190 -0
  50. package/src/barlist/__tests__/compile-barlist.test.ts +200 -0
  51. package/src/barlist/compile-barlist.ts +391 -0
  52. package/src/barlist/types.ts +28 -0
  53. package/src/charts/__tests__/post-process.test.ts +237 -0
  54. package/src/charts/__tests__/registry.test.ts +116 -0
  55. package/src/charts/__tests__/utils.test.ts +195 -0
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +32 -0
  57. package/src/charts/_shared/density-filter.ts +26 -0
  58. package/src/charts/_shared/format-label-value.ts +16 -0
  59. package/src/charts/bar/__tests__/compute.test.ts +934 -0
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +127 -0
  61. package/src/charts/bar/__tests__/labels.test.ts +292 -0
  62. package/src/charts/bar/compute.ts +539 -0
  63. package/src/charts/bar/index.ts +60 -0
  64. package/src/charts/bar/labels.ts +311 -0
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +280 -0
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +148 -0
  67. package/src/charts/beeswarm/compute.ts +203 -0
  68. package/src/charts/beeswarm/dodge.ts +155 -0
  69. package/src/charts/beeswarm/index.ts +32 -0
  70. package/src/charts/builtin.ts +75 -0
  71. package/src/charts/calendar/__tests__/compute.test.ts +417 -0
  72. package/src/charts/calendar/compute.ts +319 -0
  73. package/src/charts/calendar/index.ts +32 -0
  74. package/src/charts/column/__tests__/compute.test.ts +540 -0
  75. package/src/charts/column/__tests__/labels.test.ts +179 -0
  76. package/src/charts/column/compute.ts +493 -0
  77. package/src/charts/column/index.ts +53 -0
  78. package/src/charts/column/labels.ts +229 -0
  79. package/src/charts/dot/__tests__/compute.test.ts +380 -0
  80. package/src/charts/dot/__tests__/labels.test.ts +98 -0
  81. package/src/charts/dot/compute.ts +285 -0
  82. package/src/charts/dot/index.ts +62 -0
  83. package/src/charts/dot/labels.ts +121 -0
  84. package/src/charts/line/__tests__/compute.test.ts +1194 -0
  85. package/src/charts/line/__tests__/curves.test.ts +51 -0
  86. package/src/charts/line/__tests__/labels.test.ts +94 -0
  87. package/src/charts/line/__tests__/mark-options.test.ts +175 -0
  88. package/src/charts/line/area.ts +484 -0
  89. package/src/charts/line/compute.ts +356 -0
  90. package/src/charts/line/curves.ts +85 -0
  91. package/src/charts/line/index.ts +176 -0
  92. package/src/charts/line/labels.ts +177 -0
  93. package/src/charts/parliament/__tests__/compute.test.ts +411 -0
  94. package/src/charts/parliament/compute.ts +313 -0
  95. package/src/charts/parliament/index.ts +32 -0
  96. package/src/charts/pie/__tests__/compute.test.ts +384 -0
  97. package/src/charts/pie/__tests__/labels.test.ts +132 -0
  98. package/src/charts/pie/compute.ts +324 -0
  99. package/src/charts/pie/index.ts +49 -0
  100. package/src/charts/pie/labels.ts +143 -0
  101. package/src/charts/post-process.ts +229 -0
  102. package/src/charts/range/__tests__/compute.test.ts +486 -0
  103. package/src/charts/range/compute.ts +273 -0
  104. package/src/charts/range/index.ts +82 -0
  105. package/src/charts/range/labels.ts +169 -0
  106. package/src/charts/rect/__tests__/compute.test.ts +120 -0
  107. package/src/charts/rect/index.ts +127 -0
  108. package/src/charts/registry.ts +72 -0
  109. package/src/charts/rule/index.ts +139 -0
  110. package/src/charts/scatter/__tests__/compute.test.ts +488 -0
  111. package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
  112. package/src/charts/scatter/compute.ts +192 -0
  113. package/src/charts/scatter/index.ts +46 -0
  114. package/src/charts/scatter/trendline.ts +100 -0
  115. package/src/charts/text/__tests__/compute.test.ts +307 -0
  116. package/src/charts/text/index.ts +141 -0
  117. package/src/charts/tick/__tests__/compute.test.ts +75 -0
  118. package/src/charts/tick/index.ts +99 -0
  119. package/src/charts/utils.ts +170 -0
  120. package/src/charts/waffle/__tests__/compute.test.ts +305 -0
  121. package/src/charts/waffle/compute.ts +197 -0
  122. package/src/charts/waffle/index.ts +31 -0
  123. package/src/compile/__tests__/color-scale-range.test.ts +158 -0
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +89 -0
  125. package/src/compile/__tests__/data-clip.test.ts +127 -0
  126. package/src/compile/__tests__/fill-patterns.test.ts +154 -0
  127. package/src/compile/__tests__/size-scale.test.ts +108 -0
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +93 -0
  129. package/src/compile/color-scale-range.ts +101 -0
  130. package/src/compile/data-clip.ts +48 -0
  131. package/src/compile/fill-patterns.ts +74 -0
  132. package/src/compile/layer.ts +626 -0
  133. package/src/compile/size-scale.ts +141 -0
  134. package/src/compile/spec-sugar.ts +758 -0
  135. package/src/compile/watermark-obstacle.ts +54 -0
  136. package/src/compile/you-draw-it.ts +123 -0
  137. package/src/compile.ts +1450 -0
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +148 -0
  139. package/src/compiler/__tests__/animation.test.ts +177 -0
  140. package/src/compiler/__tests__/compile.test.ts +87 -0
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +19 -0
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +20 -0
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +22 -0
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +19 -0
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +14 -0
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +34 -0
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +22 -0
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +41 -0
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +38 -0
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +28 -0
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +28 -0
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +30 -0
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +29 -0
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +32 -0
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +25 -0
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +19 -0
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +19 -0
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +21 -0
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +23 -0
  160. package/src/compiler/__tests__/keys.test.ts +83 -0
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +103 -0
  162. package/src/compiler/__tests__/normalize.test.ts +399 -0
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +153 -0
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +86 -0
  165. package/src/compiler/__tests__/validate-highlight.test.ts +112 -0
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +89 -0
  167. package/src/compiler/__tests__/validate.test.ts +629 -0
  168. package/src/compiler/__tests__/vl-parity.test.ts +68 -0
  169. package/src/compiler/a11y-warnings.ts +110 -0
  170. package/src/compiler/animation.ts +142 -0
  171. package/src/compiler/index.ts +47 -0
  172. package/src/compiler/keys.ts +55 -0
  173. package/src/compiler/normalize.ts +734 -0
  174. package/src/compiler/sparkline-defaults.ts +138 -0
  175. package/src/compiler/types.ts +241 -0
  176. package/src/compiler/validate.ts +1871 -0
  177. package/src/endpoint-labels/__tests__/compute.test.ts +587 -0
  178. package/src/endpoint-labels/compute.ts +621 -0
  179. package/src/endpoint-labels/constants.ts +54 -0
  180. package/src/endpoint-labels/format.ts +60 -0
  181. package/src/endpoint-labels/predict.ts +139 -0
  182. package/src/format/__tests__/field-format.test.ts +164 -0
  183. package/src/format/field-format.ts +27 -0
  184. package/src/geo/__tests__/compile-map.test.ts +832 -0
  185. package/src/geo/compile-map.ts +1243 -0
  186. package/src/geo/join.ts +115 -0
  187. package/src/geo/projections.ts +45 -0
  188. package/src/geo/types.ts +26 -0
  189. package/src/graphs/__tests__/community.test.ts +228 -0
  190. package/src/graphs/__tests__/compile-graph.test.ts +396 -0
  191. package/src/graphs/__tests__/encoding.test.ts +415 -0
  192. package/src/graphs/community.ts +92 -0
  193. package/src/graphs/compile-graph.ts +329 -0
  194. package/src/graphs/encoding.ts +356 -0
  195. package/src/graphs/types.ts +106 -0
  196. package/src/index.ts +177 -0
  197. package/src/layout/__tests__/metrics.test.ts +97 -0
  198. package/src/layout/__tests__/scales-highlight.test.ts +192 -0
  199. package/src/layout/axes/rotation.ts +152 -0
  200. package/src/layout/axes/thinning.ts +98 -0
  201. package/src/layout/axes/ticks.ts +409 -0
  202. package/src/layout/axes.ts +757 -0
  203. package/src/layout/dimensions.ts +912 -0
  204. package/src/layout/facet.ts +91 -0
  205. package/src/layout/gridlines.ts +84 -0
  206. package/src/layout/metrics.ts +132 -0
  207. package/src/layout/plan.ts +631 -0
  208. package/src/layout/scales.ts +1053 -0
  209. package/src/layout/shared.ts +44 -0
  210. package/src/legend/__tests__/continuous.test.ts +318 -0
  211. package/src/legend/__tests__/redundancy.test.ts +108 -0
  212. package/src/legend/__tests__/size.test.ts +348 -0
  213. package/src/legend/__tests__/suppression.test.ts +382 -0
  214. package/src/legend/compute.ts +747 -0
  215. package/src/legend/continuous.ts +327 -0
  216. package/src/legend/size.ts +407 -0
  217. package/src/legend/suppression.ts +226 -0
  218. package/src/legend/wrap.ts +117 -0
  219. package/src/sankey/__tests__/compile-sankey.test.ts +458 -0
  220. package/src/sankey/__tests__/layout.test.ts +165 -0
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +114 -0
  222. package/src/sankey/__tests__/node-sort.test.ts +45 -0
  223. package/src/sankey/compile-sankey.ts +706 -0
  224. package/src/sankey/layout.ts +184 -0
  225. package/src/sankey/types.ts +41 -0
  226. package/src/tables/__tests__/bar-column.test.ts +147 -0
  227. package/src/tables/__tests__/category-colors.test.ts +230 -0
  228. package/src/tables/__tests__/compile-table.test.ts +236 -0
  229. package/src/tables/__tests__/format-cells.test.ts +143 -0
  230. package/src/tables/__tests__/heatmap.test.ts +148 -0
  231. package/src/tables/__tests__/pagination.test.ts +78 -0
  232. package/src/tables/__tests__/search.test.ts +94 -0
  233. package/src/tables/__tests__/sort.test.ts +107 -0
  234. package/src/tables/__tests__/sparkline.test.ts +122 -0
  235. package/src/tables/bar-column.ts +94 -0
  236. package/src/tables/category-colors.ts +78 -0
  237. package/src/tables/compile-table.ts +427 -0
  238. package/src/tables/format-cells.ts +115 -0
  239. package/src/tables/heatmap.ts +141 -0
  240. package/src/tables/pagination.ts +46 -0
  241. package/src/tables/search.ts +66 -0
  242. package/src/tables/sort.ts +69 -0
  243. package/src/tables/sparkline.ts +113 -0
  244. package/src/tables/utils.ts +16 -0
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +576 -0
  246. package/src/tilemap/compile-tilemap.ts +755 -0
  247. package/src/tilemap/layout.ts +172 -0
  248. package/src/tilemap/types.ts +33 -0
  249. package/src/tooltips/__tests__/compute.test.ts +582 -0
  250. package/src/tooltips/compute.ts +700 -0
  251. package/src/transforms/__tests__/aggregate.test.ts +159 -0
  252. package/src/transforms/__tests__/bin.test.ts +88 -0
  253. package/src/transforms/__tests__/calculate.test.ts +146 -0
  254. package/src/transforms/__tests__/conditional.test.ts +127 -0
  255. package/src/transforms/__tests__/filter-relative.test.ts +202 -0
  256. package/src/transforms/__tests__/filter.test.ts +59 -0
  257. package/src/transforms/__tests__/fold.test.ts +79 -0
  258. package/src/transforms/__tests__/index.test.ts +93 -0
  259. package/src/transforms/__tests__/predicates.test.ts +193 -0
  260. package/src/transforms/__tests__/timeunit.test.ts +129 -0
  261. package/src/transforms/__tests__/window.test.ts +286 -0
  262. package/src/transforms/aggregate.ts +131 -0
  263. package/src/transforms/bin.ts +87 -0
  264. package/src/transforms/calculate.ts +60 -0
  265. package/src/transforms/conditional.ts +46 -0
  266. package/src/transforms/filter.ts +122 -0
  267. package/src/transforms/fold.ts +49 -0
  268. package/src/transforms/index.ts +60 -0
  269. package/src/transforms/predicates.ts +123 -0
  270. package/src/transforms/timeunit.ts +88 -0
  271. package/src/transforms/window.ts +185 -0
@@ -0,0 +1,316 @@
1
+ import type { Rect } from '@opendata-ai/openchart-core';
2
+ import { overlapArea } from '@opendata-ai/openchart-core';
3
+ import { describe, expect, it } from 'vitest';
4
+ import {
5
+ findBestPlacement,
6
+ isAutoPlacement,
7
+ type PlacementObstacle,
8
+ samplePolylineObstacles,
9
+ } from '../placement';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // samplePolylineObstacles
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('samplePolylineObstacles', () => {
16
+ it('returns empty array for empty input', () => {
17
+ expect(samplePolylineObstacles([], 'line')).toEqual([]);
18
+ });
19
+
20
+ it('returns single padded rect for a single point', () => {
21
+ const pad = 3;
22
+ const result = samplePolylineObstacles([{ x: 50, y: 100 }], 'line', 24, pad);
23
+ expect(result).toHaveLength(1);
24
+ expect(result[0]).toEqual({
25
+ x: 50 - pad,
26
+ y: 100 - pad,
27
+ width: pad * 2,
28
+ height: pad * 2,
29
+ kind: 'line',
30
+ });
31
+ });
32
+
33
+ it('returns single vertical rect when all points share the same x', () => {
34
+ const pad = 3;
35
+ const points = [
36
+ { x: 100, y: 10 },
37
+ { x: 100, y: 50 },
38
+ { x: 100, y: 30 },
39
+ ];
40
+ const result = samplePolylineObstacles(points, 'line', 24, pad);
41
+ expect(result).toHaveLength(1);
42
+ expect(result[0].x).toBe(100 - pad);
43
+ expect(result[0].y).toBe(10 - pad);
44
+ expect(result[0].width).toBe(pad * 2);
45
+ expect(result[0].height).toBe(50 - 10 + pad * 2);
46
+ });
47
+
48
+ it('produces gap-free coverage for a steep sine wave', () => {
49
+ // Generate a sine wave with enough amplitude to cross many buckets vertically
50
+ const bucketWidth = 20;
51
+ const points: Array<{ x: number; y: number }> = [];
52
+ for (let i = 0; i <= 200; i++) {
53
+ const x = i;
54
+ const y = Math.sin(x * 0.1) * 100;
55
+ points.push({ x, y });
56
+ }
57
+
58
+ const obstacles = samplePolylineObstacles(points, 'line', bucketWidth, 0);
59
+ expect(obstacles.length).toBeGreaterThan(0);
60
+
61
+ // The union of obstacle x-ranges should cover the entire polyline x-extent
62
+ const xMin = Math.min(...points.map((p) => p.x));
63
+ const xMax = Math.max(...points.map((p) => p.x));
64
+
65
+ // Sort obstacles by x and verify continuous coverage
66
+ const sorted = [...obstacles].sort((a, b) => a.x - b.x);
67
+ expect(sorted[0].x).toBeLessThanOrEqual(xMin);
68
+ const lastObs = sorted[sorted.length - 1];
69
+ expect(lastObs.x + lastObs.width).toBeGreaterThanOrEqual(xMax);
70
+
71
+ // Check no gaps between consecutive obstacles
72
+ for (let i = 1; i < sorted.length; i++) {
73
+ const prevEnd = sorted[i - 1].x + sorted[i - 1].width;
74
+ expect(sorted[i].x).toBeLessThanOrEqual(prevEnd + 0.001);
75
+ }
76
+ });
77
+
78
+ it('captures min/max y within each bucket for monotone increasing data', () => {
79
+ const bucketWidth = 50;
80
+ const pad = 0;
81
+ // Monotone increasing: y = x
82
+ const points: Array<{ x: number; y: number }> = [];
83
+ for (let i = 0; i <= 200; i += 10) {
84
+ points.push({ x: i, y: i });
85
+ }
86
+
87
+ const obstacles = samplePolylineObstacles(points, 'line', bucketWidth, pad);
88
+
89
+ for (const obs of obstacles) {
90
+ // Find all points within this bucket's x-range
91
+ const bucketXStart = obs.x;
92
+ const bucketXEnd = obs.x + obs.width;
93
+ const inBucket = points.filter((p) => p.x >= bucketXStart && p.x < bucketXEnd);
94
+
95
+ if (inBucket.length === 0) continue;
96
+
97
+ const minY = Math.min(...inBucket.map((p) => p.y));
98
+ const maxY = Math.max(...inBucket.map((p) => p.y));
99
+
100
+ // The obstacle's y range should contain the points' y range within this bucket
101
+ // (it may be larger due to interpolation at bucket edges, but should not be smaller)
102
+ expect(obs.y).toBeLessThanOrEqual(minY);
103
+ expect(obs.y + obs.height).toBeGreaterThanOrEqual(maxY);
104
+ }
105
+ });
106
+
107
+ it('bucket width parameter controls granularity', () => {
108
+ const points = [
109
+ { x: 0, y: 0 },
110
+ { x: 100, y: 100 },
111
+ ];
112
+
113
+ const narrow = samplePolylineObstacles(points, 'line', 10, 0);
114
+ const wide = samplePolylineObstacles(points, 'line', 50, 0);
115
+
116
+ // Narrower buckets should produce more obstacles
117
+ expect(narrow.length).toBeGreaterThan(wide.length);
118
+ // Each obstacle's width matches the bucket width
119
+ for (const obs of narrow) expect(obs.width).toBe(10);
120
+ for (const obs of wide) expect(obs.width).toBe(50);
121
+ });
122
+ });
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // overlapArea (from @opendata-ai/openchart-core)
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe('overlapArea', () => {
129
+ it('returns 0 for disjoint rects', () => {
130
+ const a: Rect = { x: 0, y: 0, width: 10, height: 10 };
131
+ const b: Rect = { x: 20, y: 20, width: 10, height: 10 };
132
+ expect(overlapArea(a, b)).toBe(0);
133
+ });
134
+
135
+ it('returns correct area for partial overlap', () => {
136
+ const a: Rect = { x: 0, y: 0, width: 10, height: 10 };
137
+ const b: Rect = { x: 5, y: 5, width: 10, height: 10 };
138
+ // Overlap region: x=[5,10], y=[5,10] => 5*5 = 25
139
+ expect(overlapArea(a, b)).toBe(25);
140
+ });
141
+
142
+ it('returns smaller rects area when one rect is fully contained', () => {
143
+ const outer: Rect = { x: 0, y: 0, width: 100, height: 100 };
144
+ const inner: Rect = { x: 10, y: 10, width: 20, height: 30 };
145
+ expect(overlapArea(outer, inner)).toBe(20 * 30);
146
+ // Symmetric
147
+ expect(overlapArea(inner, outer)).toBe(20 * 30);
148
+ });
149
+
150
+ it('returns full area for identical rects', () => {
151
+ const r: Rect = { x: 5, y: 5, width: 40, height: 60 };
152
+ expect(overlapArea(r, r)).toBe(40 * 60);
153
+ });
154
+ });
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Scorer (via findBestPlacement)
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe('findBestPlacement scorer', () => {
161
+ // A large chart area and SVG rect so nothing clips
162
+ const chartArea: Rect = { x: 0, y: 0, width: 800, height: 600 };
163
+ const svgRect: Rect = { x: 0, y: 0, width: 800, height: 600 };
164
+ const style = { fontSize: 12, fontWeight: 400, lineHeight: 1.3 };
165
+
166
+ it('picks NE (direction index 1) first for a lower-half anchor on an empty chart', () => {
167
+ // Anchor in the lower half of the chart
168
+ const result = findBestPlacement(
169
+ 400,
170
+ 400,
171
+ 'Test',
172
+ style,
173
+ undefined,
174
+ undefined,
175
+ [],
176
+ chartArea,
177
+ svgRect,
178
+ undefined,
179
+ true,
180
+ );
181
+
182
+ // LOWER_HALF_ORDER = [1, 0, 7, 2, 3, 4, 5, 6] => first tried is NE (index 1)
183
+ // On empty chart, the first direction tried gets the lowest tiebreak and wins
184
+ // (all candidates score close to 0 for obstacles/clipping, tiebreak resolves it)
185
+ expect(result.debug).toBeDefined();
186
+ // The best candidate should be direction index 1 (NE)
187
+ const best = result.debug!.reduce((a, b) => (a.total <= b.total ? a : b));
188
+ expect(best.direction).toBe('NE');
189
+ });
190
+
191
+ it('picks SE (direction index 3) first for an upper-half anchor on an empty chart', () => {
192
+ // Anchor in the upper half of the chart
193
+ const result = findBestPlacement(
194
+ 400,
195
+ 100,
196
+ 'Test',
197
+ style,
198
+ undefined,
199
+ undefined,
200
+ [],
201
+ chartArea,
202
+ svgRect,
203
+ undefined,
204
+ true,
205
+ );
206
+
207
+ // UPPER_HALF_ORDER = [3, 4, 5, 2, 1, 0, 7, 6] => first tried is SE (index 3)
208
+ const best = result.debug!.reduce((a, b) => (a.total <= b.total ? a : b));
209
+ expect(best.direction).toBe('SE');
210
+ });
211
+
212
+ it('avoids the N direction when blocked by a hard obstacle', () => {
213
+ // Place a large hard obstacle directly above the anchor so that the N direction
214
+ // would overlap it heavily
215
+ const anchorX = 400;
216
+ const anchorY = 400;
217
+ const obstacle: PlacementObstacle = {
218
+ x: anchorX - 100,
219
+ y: anchorY - 80,
220
+ width: 200,
221
+ height: 60,
222
+ kind: 'line',
223
+ };
224
+
225
+ const result = findBestPlacement(
226
+ anchorX,
227
+ anchorY,
228
+ 'Test label',
229
+ style,
230
+ undefined,
231
+ undefined,
232
+ [obstacle],
233
+ chartArea,
234
+ svgRect,
235
+ undefined,
236
+ true,
237
+ );
238
+
239
+ // The winning direction should NOT be N (index 0), because the obstacle blocks it
240
+ const best = result.debug!.reduce((a, b) => (a.total <= b.total ? a : b));
241
+ expect(best.direction).not.toBe('N');
242
+ });
243
+
244
+ it('penalizes area-fill overlap less than line overlap of equal area', () => {
245
+ const anchorX = 400;
246
+ const anchorY = 300;
247
+
248
+ // Surround the anchor with obstacles on all sides so every candidate
249
+ // overlaps at least one. The winning candidate will necessarily overlap,
250
+ // making the obstacle kind weight the differentiating factor.
251
+ function makeWall(kind: 'line' | 'area-fill'): PlacementObstacle[] {
252
+ return [{ x: anchorX - 200, y: anchorY - 200, width: 400, height: 400, kind }];
253
+ }
254
+
255
+ const lineResult = findBestPlacement(
256
+ anchorX,
257
+ anchorY,
258
+ 'Test',
259
+ style,
260
+ undefined,
261
+ undefined,
262
+ makeWall('line'),
263
+ chartArea,
264
+ svgRect,
265
+ undefined,
266
+ true,
267
+ );
268
+
269
+ const areaResult = findBestPlacement(
270
+ anchorX,
271
+ anchorY,
272
+ 'Test',
273
+ style,
274
+ undefined,
275
+ undefined,
276
+ makeWall('area-fill'),
277
+ chartArea,
278
+ svgRect,
279
+ undefined,
280
+ true,
281
+ );
282
+
283
+ // Both winners overlap their obstacle, but area-fill weight (1.5) is much
284
+ // lower than line weight (20), so the area-fill score should be smaller.
285
+ expect(areaResult.score).toBeLessThan(lineResult.score);
286
+ });
287
+ });
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // isAutoPlacement
291
+ // ---------------------------------------------------------------------------
292
+
293
+ describe('isAutoPlacement', () => {
294
+ it('returns true for plain annotation without offset/anchor/drop-line', () => {
295
+ expect(isAutoPlacement({})).toBe(true);
296
+ });
297
+
298
+ it('returns false when offset is set', () => {
299
+ expect(isAutoPlacement({ offset: { dx: 10 } })).toBe(false);
300
+ });
301
+
302
+ it('returns false when anchor is not auto', () => {
303
+ expect(isAutoPlacement({ anchor: 'top' })).toBe(false);
304
+ expect(isAutoPlacement({ anchor: 'left' })).toBe(false);
305
+ expect(isAutoPlacement({ anchor: 'right' })).toBe(false);
306
+ expect(isAutoPlacement({ anchor: 'bottom' })).toBe(false);
307
+ });
308
+
309
+ it('returns false when connector is drop-line', () => {
310
+ expect(isAutoPlacement({ connector: 'drop-line' })).toBe(false);
311
+ });
312
+
313
+ it('returns true when anchor is explicitly auto', () => {
314
+ expect(isAutoPlacement({ anchor: 'auto' })).toBe(true);
315
+ });
316
+ });