@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,115 @@
1
+ import type { CompileWarning } from '@opendata-ai/openchart-core';
2
+
3
+ export function normalizeFips(value: string | number, digits: 2 | 5 = 2): string {
4
+ return String(value).padStart(digits, '0');
5
+ }
6
+
7
+ function detectKeyFormat(keys: string[]): string | null {
8
+ if (keys.length === 0) return null;
9
+ const allNumeric = keys.every((k) => /^\d+$/.test(k));
10
+ if (allNumeric) {
11
+ const maxLen = Math.max(...keys.map((k) => k.length));
12
+ if (maxLen <= 2) return 'state-fips';
13
+ if (maxLen <= 3) return 'iso-numeric';
14
+ if (maxLen <= 5) return 'county-fips';
15
+ }
16
+ if (keys.every((k) => /^[A-Z]{2}$/.test(k))) return 'iso-alpha2';
17
+ if (keys.every((k) => /^[A-Z]{3}$/.test(k))) return 'iso-alpha3';
18
+ return null;
19
+ }
20
+
21
+ function suggestNormalization(unmatchedKeys: string[], featureIds: string[]): string | null {
22
+ const dataFormat = detectKeyFormat(unmatchedKeys);
23
+ const featureFormat = detectKeyFormat(featureIds);
24
+
25
+ if (dataFormat === 'state-fips' && featureFormat === 'state-fips') {
26
+ const needsPadding = unmatchedKeys.some((k) => k.length === 1);
27
+ if (needsPadding) return 'Data keys appear to need zero-padding (e.g. "1" -> "01").';
28
+ }
29
+ if (dataFormat === 'county-fips') {
30
+ const needsPadding = unmatchedKeys.some((k) => k.length < 5);
31
+ if (needsPadding)
32
+ return 'County FIPS codes should be 5 digits with zero-padding (e.g. "1001" -> "01001").';
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export interface JoinResult {
38
+ joined: Map<string | number, Record<string, unknown>>;
39
+ warnings: CompileWarning[];
40
+ }
41
+
42
+ export function joinDataToFeatures(
43
+ features: Array<{ id: string | number; properties: Record<string, unknown> }>,
44
+ data: Record<string, unknown>[],
45
+ dataKeyField: string,
46
+ featureIdField: string,
47
+ ): JoinResult {
48
+ const warnings: CompileWarning[] = [];
49
+
50
+ const dataLookup = new Map<string, Record<string, unknown>>();
51
+ for (const row of data) {
52
+ const key = String(row[dataKeyField]);
53
+ dataLookup.set(key, row);
54
+ }
55
+
56
+ const joined = new Map<string | number, Record<string, unknown>>();
57
+ const usedDataKeys = new Set<string>();
58
+ let lowerLookup: Map<string, Record<string, unknown>> | null = null;
59
+ let unmatchedFeatureCount = 0;
60
+
61
+ for (const feature of features) {
62
+ const featureId =
63
+ featureIdField === 'id'
64
+ ? String(feature.id)
65
+ : String(feature.properties[featureIdField] ?? feature.id);
66
+
67
+ let match = dataLookup.get(featureId);
68
+
69
+ // Try case-insensitive fallback for name-based joins
70
+ if (!match) {
71
+ if (!lowerLookup) {
72
+ lowerLookup = new Map<string, Record<string, unknown>>();
73
+ for (const [key, row] of dataLookup) {
74
+ lowerLookup.set(key.toLowerCase(), row);
75
+ }
76
+ }
77
+ match = lowerLookup.get(featureId.toLowerCase());
78
+ }
79
+
80
+ if (match) {
81
+ joined.set(feature.id, match);
82
+ usedDataKeys.add(String(match[dataKeyField]));
83
+ } else {
84
+ unmatchedFeatureCount++;
85
+ }
86
+ }
87
+
88
+ const unmatchedDataKeys: string[] = [];
89
+ for (const key of dataLookup.keys()) {
90
+ if (!usedDataKeys.has(key)) {
91
+ unmatchedDataKeys.push(key);
92
+ }
93
+ }
94
+
95
+ if (unmatchedDataKeys.length > 0) {
96
+ const sampleKeys = unmatchedDataKeys.slice(0, 5);
97
+ const sampleFeatureIds = features.slice(0, 3).map((f) => String(f.id));
98
+ const suggestion = suggestNormalization(unmatchedDataKeys, sampleFeatureIds);
99
+ warnings.push({
100
+ code: 'UNMATCHED_DATA_KEYS',
101
+ message: `${unmatchedDataKeys.length} data row(s) matched no geo feature. Keys: ${sampleKeys.join(', ')}${unmatchedDataKeys.length > 5 ? ` (and ${unmatchedDataKeys.length - 5} more)` : ''}.${suggestion ? ` ${suggestion}` : ''} Expected format matches feature IDs like: ${sampleFeatureIds.join(', ')}.`,
102
+ context: { keys: unmatchedDataKeys },
103
+ });
104
+ }
105
+
106
+ if (unmatchedFeatureCount > 0) {
107
+ warnings.push({
108
+ code: 'UNMATCHED_FEATURES',
109
+ message: `${unmatchedFeatureCount} geo feature(s) have no matching data and will use neutral fill.`,
110
+ context: { count: unmatchedFeatureCount },
111
+ });
112
+ }
113
+
114
+ return { joined, warnings };
115
+ }
@@ -0,0 +1,45 @@
1
+ import type { MapProjection } from '@opendata-ai/openchart-core';
2
+ import type { GeoIdentityTransform, GeoPermissibleObjects, GeoProjection } from 'd3-geo';
3
+ import { geoAlbersUsa, geoBounds, geoEqualEarth, geoIdentity, geoMercator } from 'd3-geo';
4
+
5
+ // Mercator inflates polar regions toward infinity. Fitting against features
6
+ // entirely below this latitude (Antarctica) wastes most of the map area on
7
+ // empty ocean. Exclude them from the fit; they still render if in view.
8
+ const MERCATOR_FIT_MIN_LAT = -60;
9
+
10
+ export function createProjection(
11
+ type: MapProjection,
12
+ width: number,
13
+ height: number,
14
+ geojson: GeoPermissibleObjects,
15
+ ): GeoProjection | GeoIdentityTransform {
16
+ if (type === 'identity') {
17
+ return geoIdentity().fitSize([width, height], geojson);
18
+ }
19
+
20
+ const projection =
21
+ type === 'albersUsa' ? geoAlbersUsa() : type === 'equalEarth' ? geoEqualEarth() : geoMercator();
22
+
23
+ const fitGeometry = type === 'mercator' ? excludePolarFeatures(geojson) : geojson;
24
+ projection.fitSize([width, height], fitGeometry);
25
+ return projection;
26
+ }
27
+
28
+ function excludePolarFeatures(geojson: GeoPermissibleObjects): GeoPermissibleObjects {
29
+ // GeoPermissibleObjects doesn't include FeatureCollection in its type union,
30
+ // but topojson.feature() returns one at runtime. Work with the runtime shape.
31
+ const obj = geojson as unknown as Record<string, unknown>;
32
+ if (obj.type !== 'FeatureCollection' || !Array.isArray(obj.features)) return geojson;
33
+
34
+ const features = obj.features as Array<Record<string, unknown>>;
35
+ const filtered = features.filter((f) => {
36
+ try {
37
+ const b = geoBounds(f as unknown as GeoPermissibleObjects);
38
+ return b[1][1] > MERCATOR_FIT_MIN_LAT;
39
+ } catch {
40
+ return true;
41
+ }
42
+ });
43
+ if (filtered.length === 0) return geojson;
44
+ return { type: 'FeatureCollection', features: filtered } as unknown as GeoPermissibleObjects;
45
+ }
@@ -0,0 +1,26 @@
1
+ import type {
2
+ AnimationSpec,
3
+ DarkMode,
4
+ LegendConfig,
5
+ MapEncoding,
6
+ MapGeo,
7
+ MapPointsLayer,
8
+ ThemeConfig,
9
+ } from '@opendata-ai/openchart-core';
10
+
11
+ import type { NormalizedChrome } from '../compiler/types';
12
+
13
+ export interface NormalizedMapSpec {
14
+ type: 'map';
15
+ geo: Required<MapGeo>;
16
+ data: Record<string, unknown>[];
17
+ encoding: MapEncoding;
18
+ chrome: NormalizedChrome;
19
+ legend?: LegendConfig;
20
+ theme: ThemeConfig;
21
+ darkMode: DarkMode;
22
+ watermark: boolean;
23
+ animation?: AnimationSpec;
24
+ valueFormat?: string;
25
+ points?: MapPointsLayer;
26
+ }
@@ -0,0 +1,228 @@
1
+ import { resolveTheme } from '@opendata-ai/openchart-core';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { applyCommunityColors, assignCommunities, buildCommunityColorMap } from '../community';
4
+ import type { CompiledGraphNode } from '../types';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Shared fixtures
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const theme = resolveTheme({});
11
+
12
+ function makeNodes(): CompiledGraphNode[] {
13
+ return [
14
+ {
15
+ id: 'a',
16
+ radius: 5,
17
+ fill: '#aaa',
18
+ stroke: '#888',
19
+ strokeWidth: 1,
20
+ label: 'A',
21
+ labelPriority: 0.5,
22
+ community: undefined,
23
+ data: { id: 'a', group: 'X', department: 'eng' },
24
+ },
25
+ {
26
+ id: 'b',
27
+ radius: 5,
28
+ fill: '#bbb',
29
+ stroke: '#999',
30
+ strokeWidth: 1,
31
+ label: 'B',
32
+ labelPriority: 0.3,
33
+ community: undefined,
34
+ data: { id: 'b', group: 'X', department: 'eng' },
35
+ },
36
+ {
37
+ id: 'c',
38
+ radius: 5,
39
+ fill: '#ccc',
40
+ stroke: '#aaa',
41
+ strokeWidth: 1,
42
+ label: 'C',
43
+ labelPriority: 0.8,
44
+ community: undefined,
45
+ data: { id: 'c', group: 'Y', department: 'sales' },
46
+ },
47
+ {
48
+ id: 'd',
49
+ radius: 5,
50
+ fill: '#ddd',
51
+ stroke: '#bbb',
52
+ strokeWidth: 1,
53
+ label: 'D',
54
+ labelPriority: 0.2,
55
+ community: undefined,
56
+ data: { id: 'd' }, // no group field
57
+ },
58
+ ];
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // assignCommunities tests
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe('assignCommunities', () => {
66
+ it('assigns community labels from the specified field', () => {
67
+ const nodes = makeNodes();
68
+ assignCommunities(nodes, 'group');
69
+
70
+ expect(nodes[0].community).toBe('X');
71
+ expect(nodes[1].community).toBe('X');
72
+ expect(nodes[2].community).toBe('Y');
73
+ });
74
+
75
+ it('leaves community undefined when field is missing on a node', () => {
76
+ const nodes = makeNodes();
77
+ assignCommunities(nodes, 'group');
78
+
79
+ // Node 'd' doesn't have the 'group' field
80
+ expect(nodes[3].community).toBeUndefined();
81
+ });
82
+
83
+ it('does nothing when clusteringField is undefined', () => {
84
+ const nodes = makeNodes();
85
+ assignCommunities(nodes, undefined);
86
+
87
+ for (const node of nodes) {
88
+ expect(node.community).toBeUndefined();
89
+ }
90
+ });
91
+
92
+ it('handles non-string field values by converting to string', () => {
93
+ const nodes = makeNodes();
94
+ // Add a numeric field value
95
+ nodes[0].data.numericGroup = 42;
96
+ assignCommunities(nodes, 'numericGroup');
97
+
98
+ expect(nodes[0].community).toBe('42');
99
+ });
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // buildCommunityColorMap tests
104
+ // ---------------------------------------------------------------------------
105
+
106
+ describe('buildCommunityColorMap', () => {
107
+ it('creates a color for each unique community', () => {
108
+ const nodes = makeNodes();
109
+ assignCommunities(nodes, 'group');
110
+
111
+ const colorMap = buildCommunityColorMap(nodes, theme);
112
+
113
+ expect(colorMap.size).toBe(2);
114
+ expect(colorMap.has('X')).toBe(true);
115
+ expect(colorMap.has('Y')).toBe(true);
116
+ });
117
+
118
+ it('uses categorical palette colors', () => {
119
+ const nodes = makeNodes();
120
+ assignCommunities(nodes, 'group');
121
+
122
+ const colorMap = buildCommunityColorMap(nodes, theme);
123
+
124
+ const colors = [...colorMap.values()];
125
+ const palette = theme.colors.categorical;
126
+
127
+ // First two communities should get first two palette colors
128
+ expect(colors[0]).toBe(palette[0]);
129
+ expect(colors[1]).toBe(palette[1]);
130
+ });
131
+
132
+ it('returns empty map when no communities assigned', () => {
133
+ const nodes = makeNodes();
134
+ // Don't assign communities
135
+
136
+ const colorMap = buildCommunityColorMap(nodes, theme);
137
+
138
+ expect(colorMap.size).toBe(0);
139
+ });
140
+
141
+ it('wraps around palette when more communities than colors', () => {
142
+ // Create enough nodes with unique communities to exceed palette size
143
+ const manyNodes: CompiledGraphNode[] = [];
144
+ const paletteSize = theme.colors.categorical.length;
145
+
146
+ for (let i = 0; i < paletteSize + 2; i++) {
147
+ manyNodes.push({
148
+ id: `n${i}`,
149
+ radius: 5,
150
+ fill: '#ccc',
151
+ stroke: '#aaa',
152
+ strokeWidth: 1,
153
+ label: `Node ${i}`,
154
+ labelPriority: 0,
155
+ community: `community-${i}`,
156
+ data: { id: `n${i}` },
157
+ });
158
+ }
159
+
160
+ const colorMap = buildCommunityColorMap(manyNodes, theme);
161
+
162
+ expect(colorMap.size).toBe(paletteSize + 2);
163
+ // The (paletteSize + 1)th community should wrap to palette[0]
164
+ const wrappedColor = [...colorMap.values()][paletteSize];
165
+ expect(wrappedColor).toBe(theme.colors.categorical[0]);
166
+ });
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // applyCommunityColors tests
171
+ // ---------------------------------------------------------------------------
172
+
173
+ describe('applyCommunityColors', () => {
174
+ it('overrides node fill with community color', () => {
175
+ const nodes = makeNodes();
176
+ assignCommunities(nodes, 'group');
177
+
178
+ const colorMap = buildCommunityColorMap(nodes, theme);
179
+ const xColor = colorMap.get('X')!;
180
+
181
+ applyCommunityColors(nodes, colorMap);
182
+
183
+ expect(nodes[0].fill).toBe(xColor);
184
+ expect(nodes[1].fill).toBe(xColor);
185
+ });
186
+
187
+ it('overrides node stroke with darkened community color', () => {
188
+ const nodes = makeNodes();
189
+ assignCommunities(nodes, 'group');
190
+
191
+ const colorMap = buildCommunityColorMap(nodes, theme);
192
+ const originalStroke0 = nodes[0].stroke;
193
+
194
+ applyCommunityColors(nodes, colorMap);
195
+
196
+ // Stroke should have changed from original
197
+ expect(nodes[0].stroke).not.toBe(originalStroke0);
198
+ // Stroke should be different from fill (darkened)
199
+ expect(nodes[0].stroke).not.toBe(nodes[0].fill);
200
+ });
201
+
202
+ it('leaves nodes without communities unchanged', () => {
203
+ const nodes = makeNodes();
204
+ assignCommunities(nodes, 'group');
205
+
206
+ const originalFill = nodes[3].fill;
207
+ const originalStroke = nodes[3].stroke;
208
+
209
+ const colorMap = buildCommunityColorMap(nodes, theme);
210
+ applyCommunityColors(nodes, colorMap);
211
+
212
+ // Node 'd' has no group field, community is undefined
213
+ expect(nodes[3].fill).toBe(originalFill);
214
+ expect(nodes[3].stroke).toBe(originalStroke);
215
+ });
216
+
217
+ it('nodes in the same community have the same color', () => {
218
+ const nodes = makeNodes();
219
+ assignCommunities(nodes, 'group');
220
+
221
+ const colorMap = buildCommunityColorMap(nodes, theme);
222
+ applyCommunityColors(nodes, colorMap);
223
+
224
+ // Nodes a and b are both in community 'X'
225
+ expect(nodes[0].fill).toBe(nodes[1].fill);
226
+ expect(nodes[0].stroke).toBe(nodes[1].stroke);
227
+ });
228
+ });