@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,832 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { compileMap } from '../../compile';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Shared fixtures
6
+ // ---------------------------------------------------------------------------
7
+
8
+ // Minimal valid TopoJSON with 3 US states.
9
+ // Arcs are delta-encoded as per the TopoJSON specification:
10
+ // first point is absolute, subsequent points are deltas.
11
+ const MINI_TOPO = {
12
+ type: 'Topology',
13
+ objects: {
14
+ states: {
15
+ type: 'GeometryCollection',
16
+ geometries: [
17
+ { type: 'Polygon', id: '06', properties: { name: 'California' }, arcs: [[0]] },
18
+ { type: 'Polygon', id: '48', properties: { name: 'Texas' }, arcs: [[1]] },
19
+ { type: 'Polygon', id: '36', properties: { name: 'New York' }, arcs: [[2]] },
20
+ ],
21
+ },
22
+ },
23
+ arcs: [
24
+ // California (approximate): (-122,37) -> (-118,34) -> (-121,36) -> (-122,37)
25
+ [
26
+ [-122, 37],
27
+ [4, -3],
28
+ [-3, 2],
29
+ [-1, 1],
30
+ ],
31
+ // Texas (approximate): (-100,31) -> (-94,30) -> (-97,26) -> (-100,31)
32
+ [
33
+ [-100, 31],
34
+ [6, -1],
35
+ [-3, -4],
36
+ [-3, 5],
37
+ ],
38
+ // New York (approximate): (-74,41) -> (-73,40) -> (-76,42) -> (-74,41)
39
+ [
40
+ [-74, 41],
41
+ [1, -1],
42
+ [-3, 2],
43
+ [2, -1],
44
+ ],
45
+ ],
46
+ };
47
+
48
+ const DEFAULT_OPTIONS = { width: 600, height: 400 };
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Tests
52
+ // ---------------------------------------------------------------------------
53
+
54
+ describe('compileMap', () => {
55
+ it('compiles a minimal MapSpec and produces features with paths', () => {
56
+ const layout = compileMap(
57
+ {
58
+ type: 'map',
59
+ geo: { features: MINI_TOPO, projection: 'mercator' },
60
+ data: [
61
+ { fips: '06', value: 10 },
62
+ { fips: '48', value: 20 },
63
+ { fips: '36', value: 30 },
64
+ ],
65
+ encoding: {
66
+ key: { field: 'fips', type: 'nominal' },
67
+ color: { field: 'value', type: 'quantitative' },
68
+ },
69
+ },
70
+ DEFAULT_OPTIONS,
71
+ );
72
+
73
+ expect(layout.features).toHaveLength(3);
74
+ for (const f of layout.features) {
75
+ expect(f.path).toBeTruthy();
76
+ expect(f.fill).toBeTruthy();
77
+ expect(f.type).toBe('map-feature');
78
+ }
79
+ // Non-shared arcs produce no interior mesh, but outlines should exist
80
+ expect(layout.borders.outlinePath).toBeTruthy();
81
+ expect(layout.width).toBe(600);
82
+ });
83
+
84
+ it('produces unmatched data key warnings', () => {
85
+ const layout = compileMap(
86
+ {
87
+ type: 'map',
88
+ geo: { features: MINI_TOPO, projection: 'mercator' },
89
+ data: [{ fips: '99', value: 100 }],
90
+ encoding: {
91
+ key: { field: 'fips', type: 'nominal' },
92
+ color: { field: 'value', type: 'quantitative' },
93
+ },
94
+ },
95
+ DEFAULT_OPTIONS,
96
+ );
97
+
98
+ const warning = layout.warnings.find((w) => w.code === 'UNMATCHED_DATA_KEYS');
99
+ expect(warning).toBeDefined();
100
+ expect(warning!.message).toContain('99');
101
+ });
102
+
103
+ it('produces unmatched features warning when features have no data', () => {
104
+ const layout = compileMap(
105
+ {
106
+ type: 'map',
107
+ geo: { features: MINI_TOPO, projection: 'mercator' },
108
+ data: [{ fips: '06', value: 10 }],
109
+ encoding: {
110
+ key: { field: 'fips', type: 'nominal' },
111
+ color: { field: 'value', type: 'quantitative' },
112
+ },
113
+ },
114
+ DEFAULT_OPTIONS,
115
+ );
116
+
117
+ const warning = layout.warnings.find((w) => w.code === 'UNMATCHED_FEATURES');
118
+ expect(warning).toBeDefined();
119
+ expect(warning!.message).toContain('2');
120
+ });
121
+
122
+ it('throws self-documenting error when geo.features is missing', () => {
123
+ expect(() =>
124
+ compileMap(
125
+ {
126
+ type: 'map',
127
+ geo: { features: null },
128
+ data: [],
129
+ encoding: {
130
+ key: { field: 'fips', type: 'nominal' },
131
+ color: { field: 'value', type: 'quantitative' },
132
+ },
133
+ },
134
+ DEFAULT_OPTIONS,
135
+ ),
136
+ ).toThrow(/features/);
137
+ });
138
+
139
+ it('throws when geo.features is not valid TopoJSON', () => {
140
+ expect(() =>
141
+ compileMap(
142
+ {
143
+ type: 'map',
144
+ geo: { features: { type: 'FeatureCollection', features: [] } },
145
+ data: [],
146
+ encoding: {
147
+ key: { field: 'fips', type: 'nominal' },
148
+ color: { field: 'value', type: 'quantitative' },
149
+ },
150
+ },
151
+ DEFAULT_OPTIONS,
152
+ ),
153
+ ).toThrow(/Topology/);
154
+ });
155
+
156
+ it('uses mercator projection and produces valid SVG paths', () => {
157
+ const layout = compileMap(
158
+ {
159
+ type: 'map',
160
+ geo: { features: MINI_TOPO, projection: 'mercator' },
161
+ data: [{ fips: '06', value: 10 }],
162
+ encoding: {
163
+ key: { field: 'fips', type: 'nominal' },
164
+ color: { field: 'value', type: 'quantitative' },
165
+ },
166
+ },
167
+ DEFAULT_OPTIONS,
168
+ );
169
+
170
+ expect(layout.features.length).toBeGreaterThan(0);
171
+ expect(layout.features[0].path).toMatch(/^M/);
172
+ });
173
+
174
+ it('uses equalEarth projection', () => {
175
+ const layout = compileMap(
176
+ {
177
+ type: 'map',
178
+ geo: { features: MINI_TOPO, projection: 'equalEarth' },
179
+ data: [{ fips: '06', value: 10 }],
180
+ encoding: {
181
+ key: { field: 'fips', type: 'nominal' },
182
+ color: { field: 'value', type: 'quantitative' },
183
+ },
184
+ },
185
+ DEFAULT_OPTIONS,
186
+ );
187
+
188
+ expect(layout.features.length).toBeGreaterThan(0);
189
+ });
190
+
191
+ it('includes a continuous legend for quantitative encoding', () => {
192
+ const layout = compileMap(
193
+ {
194
+ type: 'map',
195
+ geo: { features: MINI_TOPO, projection: 'mercator' },
196
+ data: [
197
+ { fips: '06', value: 10 },
198
+ { fips: '48', value: 50 },
199
+ ],
200
+ encoding: {
201
+ key: { field: 'fips', type: 'nominal' },
202
+ color: { field: 'value', type: 'quantitative' },
203
+ },
204
+ },
205
+ DEFAULT_OPTIONS,
206
+ );
207
+
208
+ expect(layout.continuousLegend).not.toBeNull();
209
+ expect(layout.continuousLegend!.type).toBe('continuous');
210
+ expect(layout.categoricalLegend).toBeNull();
211
+ });
212
+
213
+ it('continuous legend has 5 bins by default', () => {
214
+ const layout = compileMap(
215
+ {
216
+ type: 'map',
217
+ geo: { features: MINI_TOPO, projection: 'mercator' },
218
+ data: [
219
+ { fips: '06', value: 10 },
220
+ { fips: '48', value: 50 },
221
+ { fips: '36', value: 30 },
222
+ ],
223
+ encoding: {
224
+ key: { field: 'fips', type: 'nominal' },
225
+ color: { field: 'value', type: 'quantitative' },
226
+ },
227
+ },
228
+ DEFAULT_OPTIONS,
229
+ );
230
+
231
+ expect(layout.continuousLegend).not.toBeNull();
232
+ expect(layout.continuousLegend!.mode).toBe('binned');
233
+ expect(layout.continuousLegend!.bins.length).toBe(5);
234
+ });
235
+
236
+ it('continuous legend defaults to top position', () => {
237
+ const layout = compileMap(
238
+ {
239
+ type: 'map',
240
+ geo: { features: MINI_TOPO, projection: 'mercator' },
241
+ data: [
242
+ { fips: '06', value: 10 },
243
+ { fips: '48', value: 50 },
244
+ ],
245
+ encoding: {
246
+ key: { field: 'fips', type: 'nominal' },
247
+ color: { field: 'value', type: 'quantitative' },
248
+ },
249
+ },
250
+ DEFAULT_OPTIONS,
251
+ );
252
+
253
+ expect(layout.continuousLegend!.position).toBe('top');
254
+ });
255
+
256
+ it('honors position: bottom for continuous legend', () => {
257
+ const layout = compileMap(
258
+ {
259
+ type: 'map',
260
+ geo: { features: MINI_TOPO, projection: 'mercator' },
261
+ data: [
262
+ { fips: '06', value: 10 },
263
+ { fips: '48', value: 50 },
264
+ ],
265
+ encoding: {
266
+ key: { field: 'fips', type: 'nominal' },
267
+ color: { field: 'value', type: 'quantitative' },
268
+ },
269
+ legend: { position: 'bottom' },
270
+ },
271
+ DEFAULT_OPTIONS,
272
+ );
273
+
274
+ expect(layout.continuousLegend!.position).toBe('bottom');
275
+ });
276
+
277
+ it('builds categorical legend for nominal encoding', () => {
278
+ const layout = compileMap(
279
+ {
280
+ type: 'map',
281
+ geo: { features: MINI_TOPO, projection: 'mercator' },
282
+ data: [
283
+ { fips: '06', category: 'west' },
284
+ { fips: '48', category: 'south' },
285
+ ],
286
+ encoding: {
287
+ key: { field: 'fips', type: 'nominal' },
288
+ color: { field: 'category', type: 'nominal' },
289
+ },
290
+ },
291
+ DEFAULT_OPTIONS,
292
+ );
293
+
294
+ expect(layout.categoricalLegend).not.toBeNull();
295
+ expect(layout.categoricalLegend!.entries).toHaveLength(2);
296
+ expect(layout.continuousLegend).toBeNull();
297
+ });
298
+
299
+ it('assigns neutral fill to features without data', () => {
300
+ const layout = compileMap(
301
+ {
302
+ type: 'map',
303
+ geo: { features: MINI_TOPO, projection: 'mercator' },
304
+ data: [{ fips: '06', value: 10 }],
305
+ encoding: {
306
+ key: { field: 'fips', type: 'nominal' },
307
+ color: { field: 'value', type: 'quantitative' },
308
+ },
309
+ },
310
+ DEFAULT_OPTIONS,
311
+ );
312
+
313
+ const matched = layout.features.find((f) => f.id === '06');
314
+ const unmatched = layout.features.find((f) => f.id === '48');
315
+ expect(matched).toBeDefined();
316
+ expect(unmatched).toBeDefined();
317
+ expect(unmatched!.fill).not.toBe(matched!.fill);
318
+ });
319
+
320
+ it('emits warnings via onWarn callback', () => {
321
+ const warnings: string[] = [];
322
+ compileMap(
323
+ {
324
+ type: 'map',
325
+ geo: { features: MINI_TOPO, projection: 'mercator' },
326
+ data: [{ fips: '99', value: 100 }],
327
+ encoding: {
328
+ key: { field: 'fips', type: 'nominal' },
329
+ color: { field: 'value', type: 'quantitative' },
330
+ },
331
+ },
332
+ { ...DEFAULT_OPTIONS, onWarn: (msg) => warnings.push(msg) },
333
+ );
334
+
335
+ expect(warnings.length).toBeGreaterThan(0);
336
+ expect(warnings.some((w) => w.includes('99'))).toBe(true);
337
+ });
338
+
339
+ it('identity projection paths fit inside layout.area', () => {
340
+ // Pre-projected coordinates (pixel-space) that identity should scale to fit
341
+ const PRE_PROJECTED_TOPO = {
342
+ type: 'Topology',
343
+ objects: {
344
+ regions: {
345
+ type: 'GeometryCollection',
346
+ geometries: [
347
+ { type: 'Polygon', id: 'A', properties: { name: 'Region A' }, arcs: [[0]] },
348
+ { type: 'Polygon', id: 'B', properties: { name: 'Region B' }, arcs: [[1]] },
349
+ ],
350
+ },
351
+ },
352
+ arcs: [
353
+ // Region A: (0,0) -> (100,0) -> (100,50) -> (0,50) -> (0,0)
354
+ [
355
+ [0, 0],
356
+ [100, 0],
357
+ [0, 50],
358
+ [-100, 0],
359
+ [0, -50],
360
+ ],
361
+ // Region B: (100,0) -> (200,0) -> (200,50) -> (100,50) -> (100,0)
362
+ [
363
+ [100, 0],
364
+ [100, 0],
365
+ [0, 50],
366
+ [-100, 0],
367
+ [0, -50],
368
+ ],
369
+ ],
370
+ };
371
+
372
+ const layout = compileMap(
373
+ {
374
+ type: 'map',
375
+ geo: { features: PRE_PROJECTED_TOPO, projection: 'identity' },
376
+ data: [
377
+ { id: 'A', value: 10 },
378
+ { id: 'B', value: 20 },
379
+ ],
380
+ encoding: {
381
+ key: { field: 'id', type: 'nominal' },
382
+ color: { field: 'value', type: 'quantitative' },
383
+ },
384
+ },
385
+ DEFAULT_OPTIONS,
386
+ );
387
+
388
+ expect(layout.features.length).toBe(2);
389
+ // Verify paths exist and geometry actually scaled (not just passthrough)
390
+ for (const f of layout.features) {
391
+ expect(f.path).toMatch(/^M/);
392
+ // Extract coordinates from the path and verify they fit within layout area
393
+ const coords = f.path.match(/[-\d.]+/g)?.map(Number) ?? [];
394
+ expect(coords.length).toBeGreaterThan(0);
395
+ }
396
+ });
397
+
398
+ it('identity projection emits no INVERTED_WINDING warning', () => {
399
+ const warnings: string[] = [];
400
+ const layout = compileMap(
401
+ {
402
+ type: 'map',
403
+ geo: { features: MINI_TOPO, projection: 'identity' },
404
+ data: [{ fips: '06', value: 10 }],
405
+ encoding: {
406
+ key: { field: 'fips', type: 'nominal' },
407
+ color: { field: 'value', type: 'quantitative' },
408
+ },
409
+ },
410
+ { ...DEFAULT_OPTIONS, onWarn: (msg) => warnings.push(msg) },
411
+ );
412
+
413
+ const windingWarning = layout.warnings.find((w) => w.code === 'INVERTED_WINDING');
414
+ expect(windingWarning).toBeUndefined();
415
+ });
416
+
417
+ it('every feature has finite bounds and centroid within mapSize', () => {
418
+ const layout = compileMap(
419
+ {
420
+ type: 'map',
421
+ geo: { features: MINI_TOPO, projection: 'mercator' },
422
+ data: [
423
+ { fips: '06', value: 10 },
424
+ { fips: '48', value: 20 },
425
+ { fips: '36', value: 30 },
426
+ ],
427
+ encoding: {
428
+ key: { field: 'fips', type: 'nominal' },
429
+ color: { field: 'value', type: 'quantitative' },
430
+ },
431
+ },
432
+ DEFAULT_OPTIONS,
433
+ );
434
+
435
+ const { mapSize } = layout;
436
+ for (const f of layout.features) {
437
+ expect(Number.isFinite(f.bounds.x)).toBe(true);
438
+ expect(Number.isFinite(f.bounds.y)).toBe(true);
439
+ expect(Number.isFinite(f.bounds.width)).toBe(true);
440
+ expect(Number.isFinite(f.bounds.height)).toBe(true);
441
+ expect(f.bounds.width).toBeGreaterThan(0);
442
+ expect(f.bounds.height).toBeGreaterThan(0);
443
+
444
+ // Centroid should be finite and within the mapSize
445
+ expect(Number.isFinite(f.centroid[0])).toBe(true);
446
+ expect(Number.isFinite(f.centroid[1])).toBe(true);
447
+ expect(f.centroid[0]).toBeGreaterThanOrEqual(0);
448
+ expect(f.centroid[0]).toBeLessThanOrEqual(mapSize.width);
449
+ expect(f.centroid[1]).toBeGreaterThanOrEqual(0);
450
+ expect(f.centroid[1]).toBeLessThanOrEqual(mapSize.height);
451
+ }
452
+ });
453
+
454
+ it('mapSize has positive width and height', () => {
455
+ const layout = compileMap(
456
+ {
457
+ type: 'map',
458
+ geo: { features: MINI_TOPO, projection: 'mercator' },
459
+ data: [
460
+ { fips: '06', value: 10 },
461
+ { fips: '48', value: 20 },
462
+ ],
463
+ encoding: {
464
+ key: { field: 'fips', type: 'nominal' },
465
+ color: { field: 'value', type: 'quantitative' },
466
+ },
467
+ },
468
+ DEFAULT_OPTIONS,
469
+ );
470
+
471
+ expect(layout.mapSize.width).toBeGreaterThan(0);
472
+ expect(layout.mapSize.height).toBeGreaterThan(0);
473
+ });
474
+
475
+ it('resolves chrome (title, subtitle)', () => {
476
+ const layout = compileMap(
477
+ {
478
+ type: 'map',
479
+ geo: { features: MINI_TOPO, projection: 'mercator' },
480
+ data: [{ fips: '06', value: 10 }],
481
+ encoding: {
482
+ key: { field: 'fips', type: 'nominal' },
483
+ color: { field: 'value', type: 'quantitative' },
484
+ },
485
+ chrome: { title: 'Test Map', subtitle: 'Test subtitle' },
486
+ },
487
+ DEFAULT_OPTIONS,
488
+ );
489
+
490
+ expect(layout.chrome.title).toBeDefined();
491
+ expect(layout.chrome.subtitle).toBeDefined();
492
+ });
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // Focus resolution
496
+ // ---------------------------------------------------------------------------
497
+
498
+ it('geo.focus as single string produces layout.focus with matching bounds', () => {
499
+ const layout = compileMap(
500
+ {
501
+ type: 'map',
502
+ geo: { features: MINI_TOPO, projection: 'mercator', focus: '36' },
503
+ data: [
504
+ { fips: '06', value: 10 },
505
+ { fips: '48', value: 20 },
506
+ { fips: '36', value: 30 },
507
+ ],
508
+ encoding: {
509
+ key: { field: 'fips', type: 'nominal' },
510
+ color: { field: 'value', type: 'quantitative' },
511
+ },
512
+ },
513
+ DEFAULT_OPTIONS,
514
+ );
515
+
516
+ expect(layout.focus).not.toBeNull();
517
+ expect(layout.focus!.ids).toEqual(['36']);
518
+ expect(layout.focus!.target.width).toBeGreaterThan(0);
519
+ expect(layout.focus!.target.height).toBeGreaterThan(0);
520
+ expect(layout.focus!.target.padding).toBe(16);
521
+ });
522
+
523
+ it('geo.focus as array produces union bounds', () => {
524
+ const layout = compileMap(
525
+ {
526
+ type: 'map',
527
+ geo: { features: MINI_TOPO, projection: 'mercator', focus: ['36', '06'] },
528
+ data: [
529
+ { fips: '06', value: 10 },
530
+ { fips: '36', value: 30 },
531
+ ],
532
+ encoding: {
533
+ key: { field: 'fips', type: 'nominal' },
534
+ color: { field: 'value', type: 'quantitative' },
535
+ },
536
+ },
537
+ DEFAULT_OPTIONS,
538
+ );
539
+
540
+ expect(layout.focus).not.toBeNull();
541
+ expect(layout.focus!.ids).toHaveLength(2);
542
+ expect(layout.focus!.ids.map(String).sort()).toEqual(['06', '36']);
543
+ });
544
+
545
+ it('geo.focus object form with custom padding', () => {
546
+ const layout = compileMap(
547
+ {
548
+ type: 'map',
549
+ geo: {
550
+ features: MINI_TOPO,
551
+ projection: 'mercator',
552
+ focus: { features: '36', padding: 32 },
553
+ },
554
+ data: [{ fips: '36', value: 30 }],
555
+ encoding: {
556
+ key: { field: 'fips', type: 'nominal' },
557
+ color: { field: 'value', type: 'quantitative' },
558
+ },
559
+ },
560
+ DEFAULT_OPTIONS,
561
+ );
562
+
563
+ expect(layout.focus).not.toBeNull();
564
+ expect(layout.focus!.target.padding).toBe(32);
565
+ expect(layout.focus!.ids).toEqual(['36']);
566
+ });
567
+
568
+ it('geo.focus: null produces layout.focus: null', () => {
569
+ const layout = compileMap(
570
+ {
571
+ type: 'map',
572
+ geo: { features: MINI_TOPO, projection: 'mercator', focus: null },
573
+ data: [{ fips: '06', value: 10 }],
574
+ encoding: {
575
+ key: { field: 'fips', type: 'nominal' },
576
+ color: { field: 'value', type: 'quantitative' },
577
+ },
578
+ },
579
+ DEFAULT_OPTIONS,
580
+ );
581
+
582
+ expect(layout.focus).toBeNull();
583
+ });
584
+
585
+ it('unknown focus id emits FOCUS_UNMATCHED warning', () => {
586
+ const layout = compileMap(
587
+ {
588
+ type: 'map',
589
+ geo: { features: MINI_TOPO, projection: 'mercator', focus: '99' },
590
+ data: [{ fips: '06', value: 10 }],
591
+ encoding: {
592
+ key: { field: 'fips', type: 'nominal' },
593
+ color: { field: 'value', type: 'quantitative' },
594
+ },
595
+ },
596
+ DEFAULT_OPTIONS,
597
+ );
598
+
599
+ const warning = layout.warnings.find((w) => w.code === 'FOCUS_UNMATCHED');
600
+ expect(warning).toBeDefined();
601
+ expect(warning!.message).toContain('99');
602
+ expect(layout.focus).toBeNull();
603
+ });
604
+
605
+ it('no focus specified produces layout.focus: null', () => {
606
+ const layout = compileMap(
607
+ {
608
+ type: 'map',
609
+ geo: { features: MINI_TOPO, projection: 'mercator' },
610
+ data: [{ fips: '06', value: 10 }],
611
+ encoding: {
612
+ key: { field: 'fips', type: 'nominal' },
613
+ color: { field: 'value', type: 'quantitative' },
614
+ },
615
+ },
616
+ DEFAULT_OPTIONS,
617
+ );
618
+
619
+ expect(layout.focus).toBeNull();
620
+ });
621
+
622
+ // ---------------------------------------------------------------------------
623
+ // Point layer
624
+ // ---------------------------------------------------------------------------
625
+
626
+ describe('point layer', () => {
627
+ it('compiles points through projection', () => {
628
+ const layout = compileMap(
629
+ {
630
+ type: 'map',
631
+ geo: { features: MINI_TOPO, projection: 'mercator' },
632
+ data: [],
633
+ encoding: { key: { field: 'id', type: 'nominal' } },
634
+ points: {
635
+ data: [
636
+ { lat: 34, lon: -118, name: 'LA' },
637
+ { lat: 40.7, lon: -74, name: 'NYC' },
638
+ ],
639
+ longitude: { field: 'lon', type: 'quantitative' },
640
+ latitude: { field: 'lat', type: 'quantitative' },
641
+ key: { field: 'name', type: 'nominal' },
642
+ },
643
+ },
644
+ DEFAULT_OPTIONS,
645
+ );
646
+
647
+ expect(layout.pointMarks).toHaveLength(2);
648
+ for (const pm of layout.pointMarks) {
649
+ expect(Number.isFinite(pm.cx)).toBe(true);
650
+ expect(Number.isFinite(pm.cy)).toBe(true);
651
+ }
652
+ });
653
+
654
+ it('drops null-projecting points with warning', () => {
655
+ const layout = compileMap(
656
+ {
657
+ type: 'map',
658
+ geo: { features: MINI_TOPO, projection: 'albersUsa' },
659
+ data: [],
660
+ encoding: { key: { field: 'id', type: 'nominal' } },
661
+ points: {
662
+ data: [{ lat: 0, lon: 0, name: 'Pacific' }],
663
+ longitude: { field: 'lon', type: 'quantitative' },
664
+ latitude: { field: 'lat', type: 'quantitative' },
665
+ key: { field: 'name', type: 'nominal' },
666
+ },
667
+ },
668
+ DEFAULT_OPTIONS,
669
+ );
670
+
671
+ expect(layout.pointMarks).toHaveLength(0);
672
+ const warning = layout.warnings.find((w) => w.code === 'POINT_NULL_PROJECTION');
673
+ expect(warning).toBeDefined();
674
+ });
675
+
676
+ it('builds sqrt size scale for points', () => {
677
+ const layout = compileMap(
678
+ {
679
+ type: 'map',
680
+ geo: { features: MINI_TOPO, projection: 'mercator' },
681
+ data: [],
682
+ encoding: { key: { field: 'id', type: 'nominal' } },
683
+ points: {
684
+ data: [
685
+ { lat: 34, lon: -118, name: 'A', value: 10 },
686
+ { lat: 40.7, lon: -74, name: 'B', value: 1000 },
687
+ ],
688
+ longitude: { field: 'lon', type: 'quantitative' },
689
+ latitude: { field: 'lat', type: 'quantitative' },
690
+ size: { field: 'value', type: 'quantitative' },
691
+ key: { field: 'name', type: 'nominal' },
692
+ },
693
+ },
694
+ DEFAULT_OPTIONS,
695
+ );
696
+
697
+ expect(layout.pointMarks).toHaveLength(2);
698
+ const radii = layout.pointMarks.map((pm) => pm.r);
699
+ expect(radii[0]).not.toBe(radii[1]);
700
+ // Map point size scale range is [3, 20]
701
+ for (const r of radii) {
702
+ expect(r).toBeGreaterThanOrEqual(3);
703
+ expect(r).toBeLessThanOrEqual(20);
704
+ }
705
+ });
706
+
707
+ it('builds independent categorical color scale', () => {
708
+ const layout = compileMap(
709
+ {
710
+ type: 'map',
711
+ geo: { features: MINI_TOPO, projection: 'mercator' },
712
+ data: [],
713
+ encoding: { key: { field: 'id', type: 'nominal' } },
714
+ points: {
715
+ data: [
716
+ { lat: 34, lon: -118, name: 'A', cat: 'x' },
717
+ { lat: 40.7, lon: -74, name: 'B', cat: 'y' },
718
+ { lat: 37, lon: -100, name: 'C', cat: 'z' },
719
+ ],
720
+ longitude: { field: 'lon', type: 'quantitative' },
721
+ latitude: { field: 'lat', type: 'quantitative' },
722
+ color: { field: 'cat', type: 'nominal' },
723
+ key: { field: 'name', type: 'nominal' },
724
+ },
725
+ },
726
+ DEFAULT_OPTIONS,
727
+ );
728
+
729
+ expect(layout.pointMarks).toHaveLength(3);
730
+ const fills = layout.pointMarks.map((pm) => pm.fill);
731
+ // 3 different categories should produce 3 different colors
732
+ expect(new Set(fills).size).toBe(3);
733
+ });
734
+
735
+ it('basemap-only: neutral fill when no encoding.color', () => {
736
+ const layout = compileMap(
737
+ {
738
+ type: 'map',
739
+ geo: { features: MINI_TOPO, projection: 'mercator' },
740
+ data: [],
741
+ encoding: { key: { field: 'id', type: 'nominal' } },
742
+ points: {
743
+ data: [{ lat: 34, lon: -118, name: 'LA' }],
744
+ longitude: { field: 'lon', type: 'quantitative' },
745
+ latitude: { field: 'lat', type: 'quantitative' },
746
+ key: { field: 'name', type: 'nominal' },
747
+ },
748
+ },
749
+ DEFAULT_OPTIONS,
750
+ );
751
+
752
+ // Features should all have the neutral fill (#e8e8e8 in light mode)
753
+ for (const f of layout.features) {
754
+ expect(f.fill).toBe('#e8e8e8');
755
+ }
756
+ });
757
+
758
+ it('generates point tooltips with point: prefix', () => {
759
+ const layout = compileMap(
760
+ {
761
+ type: 'map',
762
+ geo: { features: MINI_TOPO, projection: 'mercator' },
763
+ data: [],
764
+ encoding: { key: { field: 'id', type: 'nominal' } },
765
+ points: {
766
+ data: [
767
+ { lat: 34, lon: -118, name: 'LA', value: 100 },
768
+ { lat: 40.7, lon: -74, name: 'NYC', value: 200 },
769
+ ],
770
+ longitude: { field: 'lon', type: 'quantitative' },
771
+ latitude: { field: 'lat', type: 'quantitative' },
772
+ tooltip: [
773
+ { field: 'name', type: 'nominal', title: 'City' },
774
+ { field: 'value', type: 'quantitative', title: 'Pop' },
775
+ ],
776
+ key: { field: 'name', type: 'nominal' },
777
+ },
778
+ },
779
+ DEFAULT_OPTIONS,
780
+ );
781
+
782
+ const pointKeys = [...layout.tooltipDescriptors.keys()].filter((k) => k.startsWith('point:'));
783
+ expect(pointKeys).toHaveLength(2);
784
+ expect(pointKeys).toContain('point:LA');
785
+ expect(pointKeys).toContain('point:NYC');
786
+
787
+ const laTooltip = layout.tooltipDescriptors.get('point:LA');
788
+ expect(laTooltip).toBeDefined();
789
+ expect(laTooltip!.fields).toHaveLength(2);
790
+ expect(laTooltip!.fields[0].label).toBe('City');
791
+ });
792
+
793
+ it('handles points alongside choropleth', () => {
794
+ const layout = compileMap(
795
+ {
796
+ type: 'map',
797
+ geo: { features: MINI_TOPO, projection: 'mercator' },
798
+ data: [
799
+ { fips: '06', value: 10 },
800
+ { fips: '48', value: 20 },
801
+ ],
802
+ encoding: {
803
+ key: { field: 'fips', type: 'nominal' },
804
+ color: { field: 'value', type: 'quantitative' },
805
+ },
806
+ points: {
807
+ data: [
808
+ { lat: 34, lon: -118, name: 'LA', pop: 400 },
809
+ { lat: 31, lon: -100, name: 'TX City', pop: 200 },
810
+ ],
811
+ longitude: { field: 'lon', type: 'quantitative' },
812
+ latitude: { field: 'lat', type: 'quantitative' },
813
+ key: { field: 'name', type: 'nominal' },
814
+ },
815
+ },
816
+ DEFAULT_OPTIONS,
817
+ );
818
+
819
+ // Choropleth features should have color-encoded fills
820
+ const matchedFeature = layout.features.find((f) => f.id === '06');
821
+ expect(matchedFeature).toBeDefined();
822
+ expect(matchedFeature!.fill).not.toBe('#e8e8e8');
823
+
824
+ // Point marks should also be present
825
+ expect(layout.pointMarks).toHaveLength(2);
826
+ for (const pm of layout.pointMarks) {
827
+ expect(Number.isFinite(pm.cx)).toBe(true);
828
+ expect(Number.isFinite(pm.cy)).toBe(true);
829
+ }
830
+ });
831
+ });
832
+ });