graphein 0.20.0 → 0.22.0

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 (319) hide show
  1. package/dist/animation/animate.d.ts +6 -1
  2. package/dist/animation/animate.d.ts.map +1 -1
  3. package/dist/animation/animate.js +2 -2
  4. package/dist/animation/animate.js.map +2 -2
  5. package/dist/animation/env.d.ts +5 -0
  6. package/dist/animation/env.d.ts.map +1 -1
  7. package/dist/animation/env.js +4 -0
  8. package/dist/animation/env.js.map +2 -2
  9. package/dist/animation/index.d.ts +1 -0
  10. package/dist/animation/index.d.ts.map +1 -1
  11. package/dist/animation/index.js +1 -0
  12. package/dist/animation/index.js.map +2 -2
  13. package/dist/animation/stagger.d.ts +13 -0
  14. package/dist/animation/stagger.d.ts.map +1 -0
  15. package/dist/animation/stagger.js +13 -0
  16. package/dist/animation/stagger.js.map +7 -0
  17. package/dist/axes/draw.d.ts +5 -1
  18. package/dist/axes/draw.d.ts.map +1 -1
  19. package/dist/axes/draw.js +13 -1
  20. package/dist/axes/draw.js.map +2 -2
  21. package/dist/axes/morph.d.ts +111 -0
  22. package/dist/axes/morph.d.ts.map +1 -0
  23. package/dist/axes/morph.js +207 -0
  24. package/dist/axes/morph.js.map +7 -0
  25. package/dist/axes/overlay.d.ts.map +1 -1
  26. package/dist/axes/overlay.js +88 -25
  27. package/dist/axes/overlay.js.map +2 -2
  28. package/dist/axes/text.d.ts +2 -0
  29. package/dist/axes/text.d.ts.map +1 -1
  30. package/dist/axes/text.js +1 -0
  31. package/dist/axes/text.js.map +2 -2
  32. package/dist/axes/tickLabels.d.ts +2 -0
  33. package/dist/axes/tickLabels.d.ts.map +1 -1
  34. package/dist/axes/tickLabels.js +2 -2
  35. package/dist/axes/tickLabels.js.map +2 -2
  36. package/dist/charts/annotations.d.ts +9 -3
  37. package/dist/charts/annotations.d.ts.map +1 -1
  38. package/dist/charts/annotations.js +82 -27
  39. package/dist/charts/annotations.js.map +2 -2
  40. package/dist/charts/area.d.ts +37 -1
  41. package/dist/charts/area.d.ts.map +1 -1
  42. package/dist/charts/area.js +35 -17
  43. package/dist/charts/area.js.map +2 -2
  44. package/dist/charts/bar.d.ts +53 -0
  45. package/dist/charts/bar.d.ts.map +1 -1
  46. package/dist/charts/bar.js +117 -83
  47. package/dist/charts/bar.js.map +2 -2
  48. package/dist/charts/box.d.ts +31 -0
  49. package/dist/charts/box.d.ts.map +1 -1
  50. package/dist/charts/box.js +93 -70
  51. package/dist/charts/box.js.map +2 -2
  52. package/dist/charts/bubbleMap.d.ts +20 -0
  53. package/dist/charts/bubbleMap.d.ts.map +1 -0
  54. package/dist/charts/bubbleMap.js +951 -0
  55. package/dist/charts/bubbleMap.js.map +7 -0
  56. package/dist/charts/bullet.d.ts.map +1 -1
  57. package/dist/charts/bullet.js +69 -51
  58. package/dist/charts/bullet.js.map +2 -2
  59. package/dist/charts/calendarHeatmap.d.ts.map +1 -1
  60. package/dist/charts/calendarHeatmap.js +56 -19
  61. package/dist/charts/calendarHeatmap.js.map +2 -2
  62. package/dist/charts/candlestick.d.ts +40 -0
  63. package/dist/charts/candlestick.d.ts.map +1 -0
  64. package/dist/charts/candlestick.js +325 -0
  65. package/dist/charts/candlestick.js.map +7 -0
  66. package/dist/charts/choropleth.d.ts.map +1 -1
  67. package/dist/charts/choropleth.js +46 -67
  68. package/dist/charts/choropleth.js.map +2 -2
  69. package/dist/charts/combo.d.ts.map +1 -1
  70. package/dist/charts/combo.js +127 -27
  71. package/dist/charts/combo.js.map +3 -3
  72. package/dist/charts/dumbbell.d.ts.map +1 -1
  73. package/dist/charts/dumbbell.js +100 -31
  74. package/dist/charts/dumbbell.js.map +2 -2
  75. package/dist/charts/funnel.d.ts.map +1 -1
  76. package/dist/charts/funnel.js +93 -28
  77. package/dist/charts/funnel.js.map +3 -3
  78. package/dist/charts/gantt.d.ts +22 -0
  79. package/dist/charts/gantt.d.ts.map +1 -0
  80. package/dist/charts/gantt.js +564 -0
  81. package/dist/charts/gantt.js.map +7 -0
  82. package/dist/charts/gauge.d.ts.map +1 -1
  83. package/dist/charts/gauge.js +30 -4
  84. package/dist/charts/gauge.js.map +2 -2
  85. package/dist/charts/geo.d.ts +87 -0
  86. package/dist/charts/geo.d.ts.map +1 -0
  87. package/dist/charts/geo.js +140 -0
  88. package/dist/charts/geo.js.map +7 -0
  89. package/dist/charts/geometry.d.ts +20 -0
  90. package/dist/charts/geometry.d.ts.map +1 -0
  91. package/dist/charts/geometry.js +25 -0
  92. package/dist/charts/geometry.js.map +7 -0
  93. package/dist/charts/heatmap.d.ts.map +1 -1
  94. package/dist/charts/heatmap.js +58 -49
  95. package/dist/charts/heatmap.js.map +2 -2
  96. package/dist/charts/histogram.d.ts +1 -1
  97. package/dist/charts/histogram.d.ts.map +1 -1
  98. package/dist/charts/histogram.js +49 -31
  99. package/dist/charts/histogram.js.map +2 -2
  100. package/dist/charts/index.d.ts +35 -1
  101. package/dist/charts/index.d.ts.map +1 -1
  102. package/dist/charts/index.js +12 -0
  103. package/dist/charts/index.js.map +2 -2
  104. package/dist/charts/legibility.d.ts +38 -0
  105. package/dist/charts/legibility.d.ts.map +1 -0
  106. package/dist/charts/legibility.js +72 -0
  107. package/dist/charts/legibility.js.map +7 -0
  108. package/dist/charts/line.d.ts +39 -1
  109. package/dist/charts/line.d.ts.map +1 -1
  110. package/dist/charts/line.js +48 -16
  111. package/dist/charts/line.js.map +2 -2
  112. package/dist/charts/matrix.d.ts +2 -1
  113. package/dist/charts/matrix.d.ts.map +1 -1
  114. package/dist/charts/matrix.js +481 -319
  115. package/dist/charts/matrix.js.map +3 -3
  116. package/dist/charts/morphFrame.d.ts +83 -0
  117. package/dist/charts/morphFrame.d.ts.map +1 -0
  118. package/dist/charts/morphFrame.js +128 -0
  119. package/dist/charts/morphFrame.js.map +7 -0
  120. package/dist/charts/pie.d.ts.map +1 -1
  121. package/dist/charts/pie.js +89 -26
  122. package/dist/charts/pie.js.map +2 -2
  123. package/dist/charts/pieState.d.ts +27 -0
  124. package/dist/charts/pieState.d.ts.map +1 -0
  125. package/dist/charts/pieState.js +55 -0
  126. package/dist/charts/pieState.js.map +7 -0
  127. package/dist/charts/radar.d.ts +62 -0
  128. package/dist/charts/radar.d.ts.map +1 -0
  129. package/dist/charts/radar.js +912 -0
  130. package/dist/charts/radar.js.map +7 -0
  131. package/dist/charts/radialBar.d.ts +60 -0
  132. package/dist/charts/radialBar.d.ts.map +1 -0
  133. package/dist/charts/radialBar.js +708 -0
  134. package/dist/charts/radialBar.js.map +7 -0
  135. package/dist/charts/sankey.d.ts.map +1 -1
  136. package/dist/charts/sankey.js +223 -57
  137. package/dist/charts/sankey.js.map +3 -3
  138. package/dist/charts/scatter.d.ts +31 -0
  139. package/dist/charts/scatter.d.ts.map +1 -1
  140. package/dist/charts/scatter.js +48 -12
  141. package/dist/charts/scatter.js.map +2 -2
  142. package/dist/charts/slope.d.ts.map +1 -1
  143. package/dist/charts/slope.js +55 -3
  144. package/dist/charts/slope.js.map +2 -2
  145. package/dist/charts/sunburst.d.ts +14 -0
  146. package/dist/charts/sunburst.d.ts.map +1 -0
  147. package/dist/charts/sunburst.js +885 -0
  148. package/dist/charts/sunburst.js.map +7 -0
  149. package/dist/charts/table.d.ts +2 -1
  150. package/dist/charts/table.d.ts.map +1 -1
  151. package/dist/charts/table.js +214 -224
  152. package/dist/charts/table.js.map +3 -3
  153. package/dist/charts/tableView/canvas.d.ts +16 -1
  154. package/dist/charts/tableView/canvas.d.ts.map +1 -1
  155. package/dist/charts/tableView/canvas.js +361 -252
  156. package/dist/charts/tableView/canvas.js.map +3 -3
  157. package/dist/charts/tableView/conditional.d.ts +66 -40
  158. package/dist/charts/tableView/conditional.d.ts.map +1 -1
  159. package/dist/charts/tableView/conditional.js +163 -256
  160. package/dist/charts/tableView/conditional.js.map +3 -3
  161. package/dist/charts/tableView/dom.d.ts +26 -2
  162. package/dist/charts/tableView/dom.d.ts.map +1 -1
  163. package/dist/charts/tableView/dom.js +609 -269
  164. package/dist/charts/tableView/dom.js.map +3 -3
  165. package/dist/charts/tableView/layout.d.ts +22 -0
  166. package/dist/charts/tableView/layout.d.ts.map +1 -0
  167. package/dist/charts/tableView/layout.js +246 -0
  168. package/dist/charts/tableView/layout.js.map +7 -0
  169. package/dist/charts/tableView/sketch.d.ts +3 -2
  170. package/dist/charts/tableView/sketch.d.ts.map +1 -1
  171. package/dist/charts/tableView/sketch.js +2 -2
  172. package/dist/charts/tableView/sketch.js.map +2 -2
  173. package/dist/charts/tableView/sketchBar.d.ts +7 -0
  174. package/dist/charts/tableView/sketchBar.d.ts.map +1 -0
  175. package/dist/charts/tableView/sketchBar.js +32 -0
  176. package/dist/charts/tableView/sketchBar.js.map +7 -0
  177. package/dist/charts/tableView/sort.d.ts +9 -0
  178. package/dist/charts/tableView/sort.d.ts.map +1 -0
  179. package/dist/charts/tableView/sort.js +70 -0
  180. package/dist/charts/tableView/sort.js.map +7 -0
  181. package/dist/charts/tableView/sparkline.d.ts +30 -0
  182. package/dist/charts/tableView/sparkline.d.ts.map +1 -0
  183. package/dist/charts/tableView/sparkline.js +63 -0
  184. package/dist/charts/tableView/sparkline.js.map +7 -0
  185. package/dist/charts/tableView/style.d.ts +67 -0
  186. package/dist/charts/tableView/style.d.ts.map +1 -0
  187. package/dist/charts/tableView/style.js +131 -0
  188. package/dist/charts/tableView/style.js.map +7 -0
  189. package/dist/charts/tableView/types.d.ts +69 -46
  190. package/dist/charts/tableView/types.d.ts.map +1 -1
  191. package/dist/charts/tableView/types.js +22 -87
  192. package/dist/charts/tableView/types.js.map +2 -2
  193. package/dist/charts/tableView.d.ts +10 -5
  194. package/dist/charts/tableView.d.ts.map +1 -1
  195. package/dist/charts/tableView.js +12 -8
  196. package/dist/charts/tableView.js.map +2 -2
  197. package/dist/charts/treemap.d.ts.map +1 -1
  198. package/dist/charts/treemap.js +177 -68
  199. package/dist/charts/treemap.js.map +3 -3
  200. package/dist/charts/trendline.d.ts +13 -5
  201. package/dist/charts/trendline.d.ts.map +1 -1
  202. package/dist/charts/trendline.js +205 -56
  203. package/dist/charts/trendline.js.map +2 -2
  204. package/dist/charts/waterfall.d.ts.map +1 -1
  205. package/dist/charts/waterfall.js +76 -5
  206. package/dist/charts/waterfall.js.map +2 -2
  207. package/dist/index.d.ts +1 -1
  208. package/dist/index.js +1 -1
  209. package/dist/index.js.map +1 -1
  210. package/dist/interaction/cartesian.d.ts +4 -1
  211. package/dist/interaction/cartesian.d.ts.map +1 -1
  212. package/dist/interaction/cartesian.js +104 -34
  213. package/dist/interaction/cartesian.js.map +2 -2
  214. package/dist/interaction/controller.d.ts +29 -1
  215. package/dist/interaction/controller.d.ts.map +1 -1
  216. package/dist/interaction/controller.js +131 -8
  217. package/dist/interaction/controller.js.map +2 -2
  218. package/dist/interaction/focus.d.ts +23 -0
  219. package/dist/interaction/focus.d.ts.map +1 -0
  220. package/dist/interaction/focus.js +65 -0
  221. package/dist/interaction/focus.js.map +7 -0
  222. package/dist/interaction/tooltip.d.ts +6 -0
  223. package/dist/interaction/tooltip.d.ts.map +1 -1
  224. package/dist/interaction/tooltip.js +41 -16
  225. package/dist/interaction/tooltip.js.map +2 -2
  226. package/dist/interaction/types.d.ts +19 -0
  227. package/dist/interaction/types.d.ts.map +1 -1
  228. package/dist/interaction/veil.d.ts +25 -0
  229. package/dist/interaction/veil.d.ts.map +1 -0
  230. package/dist/interaction/veil.js +35 -0
  231. package/dist/interaction/veil.js.map +7 -0
  232. package/dist/pivot/pivot.d.ts +18 -0
  233. package/dist/pivot/pivot.d.ts.map +1 -1
  234. package/dist/pivot/pivot.js +46 -34
  235. package/dist/pivot/pivot.js.map +2 -2
  236. package/dist/render/chromeMotion.d.ts +65 -0
  237. package/dist/render/chromeMotion.d.ts.map +1 -0
  238. package/dist/render/chromeMotion.js +79 -0
  239. package/dist/render/chromeMotion.js.map +7 -0
  240. package/dist/render/pathSample.d.ts +24 -0
  241. package/dist/render/pathSample.d.ts.map +1 -0
  242. package/dist/render/pathSample.js +76 -0
  243. package/dist/render/pathSample.js.map +7 -0
  244. package/dist/render/transient.d.ts +18 -0
  245. package/dist/render/transient.d.ts.map +1 -0
  246. package/dist/render/transient.js +19 -0
  247. package/dist/render/transient.js.map +7 -0
  248. package/dist/runtime/candlestick.d.ts +112 -0
  249. package/dist/runtime/candlestick.d.ts.map +1 -0
  250. package/dist/runtime/candlestick.js +531 -0
  251. package/dist/runtime/candlestick.js.map +7 -0
  252. package/dist/runtime/cartesian.d.ts +14 -3
  253. package/dist/runtime/cartesian.d.ts.map +1 -1
  254. package/dist/runtime/cartesian.js.map +2 -2
  255. package/dist/runtime/chartAnimation.d.ts +54 -7
  256. package/dist/runtime/chartAnimation.d.ts.map +1 -1
  257. package/dist/runtime/chartAnimation.js +146 -28
  258. package/dist/runtime/chartAnimation.js.map +2 -2
  259. package/dist/runtime/chartRuntime.d.ts +42 -0
  260. package/dist/runtime/chartRuntime.d.ts.map +1 -1
  261. package/dist/runtime/chartRuntime.js +196 -30
  262. package/dist/runtime/chartRuntime.js.map +2 -2
  263. package/dist/runtime/dashboardDom.d.ts.map +1 -1
  264. package/dist/runtime/dashboardDom.js +45 -0
  265. package/dist/runtime/dashboardDom.js.map +2 -2
  266. package/dist/runtime/facet.d.ts +1 -1
  267. package/dist/runtime/facet.d.ts.map +1 -1
  268. package/dist/runtime/gantt.d.ts +137 -0
  269. package/dist/runtime/gantt.d.ts.map +1 -0
  270. package/dist/runtime/gantt.js +647 -0
  271. package/dist/runtime/gantt.js.map +7 -0
  272. package/dist/runtime/morph.d.ts +33 -0
  273. package/dist/runtime/morph.d.ts.map +1 -0
  274. package/dist/runtime/morph.js +332 -0
  275. package/dist/runtime/morph.js.map +7 -0
  276. package/dist/runtime/radar.d.ts +75 -0
  277. package/dist/runtime/radar.d.ts.map +1 -0
  278. package/dist/runtime/radar.js +201 -0
  279. package/dist/runtime/radar.js.map +7 -0
  280. package/dist/runtime/radialBar.d.ts +81 -0
  281. package/dist/runtime/radialBar.d.ts.map +1 -0
  282. package/dist/runtime/radialBar.js +125 -0
  283. package/dist/runtime/radialBar.js.map +7 -0
  284. package/dist/runtime/report/constants.d.ts +2 -2
  285. package/dist/runtime/report/constants.d.ts.map +1 -1
  286. package/dist/runtime/runtimeShared.d.ts +3 -3
  287. package/dist/runtime/runtimeShared.d.ts.map +1 -1
  288. package/dist/runtime/sunburst.d.ts +74 -0
  289. package/dist/runtime/sunburst.d.ts.map +1 -0
  290. package/dist/runtime/sunburst.js +150 -0
  291. package/dist/runtime/sunburst.js.map +7 -0
  292. package/dist/runtime/wire.d.ts.map +1 -1
  293. package/dist/runtime/wire.js +3 -1
  294. package/dist/runtime/wire.js.map +2 -2
  295. package/dist/shape/arc.d.ts +15 -2
  296. package/dist/shape/arc.d.ts.map +1 -1
  297. package/dist/shape/arc.js +204 -32
  298. package/dist/shape/arc.js.map +2 -2
  299. package/dist/spec/chartDefinitions.d.ts +6 -0
  300. package/dist/spec/chartDefinitions.d.ts.map +1 -1
  301. package/dist/spec/chartDefinitions.js +7 -1
  302. package/dist/spec/chartDefinitions.js.map +2 -2
  303. package/dist/spec/templates.d.ts.map +1 -1
  304. package/dist/spec/templates.js +177 -2
  305. package/dist/spec/templates.js.map +2 -2
  306. package/dist/spec/types.d.ts +258 -6
  307. package/dist/spec/types.d.ts.map +1 -1
  308. package/dist/spec/types.js +6 -0
  309. package/dist/spec/types.js.map +2 -2
  310. package/dist/spec/validate/charts.js +147 -15
  311. package/dist/spec/validate/charts.js.map +2 -2
  312. package/dist/spec/validate/common.d.ts +6 -0
  313. package/dist/spec/validate/common.d.ts.map +1 -1
  314. package/dist/spec/validate/overlays.js +2 -2
  315. package/dist/spec/validate/overlays.js.map +2 -2
  316. package/dist/spec/validate.data.d.ts.map +1 -1
  317. package/dist/spec/validate.data.js +8 -3
  318. package/dist/spec/validate.data.js.map +2 -2
  319. package/package.json +1 -1
@@ -0,0 +1,951 @@
1
+ import { contrastRatio, ordinalColorScale, parseColor, rgbaToCss, sequential, sequentialColorScale, withAlpha } from "../color/index.js";
2
+ import { formatValue } from "../format/index.js";
3
+ import { backOut } from "../animation/easing.js";
4
+ import { staggerProgress } from "../animation/stagger.js";
5
+ import { boxesOverlap, fitLabel } from "../render/labelLedger.js";
6
+ import { overlayTextToCanvasCmd } from "../render/overlayText.js";
7
+ import { fontString, measureText } from "../render/text.js";
8
+ import { RoughPen } from "../rough/index.js";
9
+ import { roundedRect } from "../shape/index.js";
10
+ import { resolveSketch } from "../spec/sketch.js";
11
+ import { accessor, inferType, toKey, toNumber } from "../util/data.js";
12
+ import { FOCUS_VEIL, hoverFocusEnabled, paintCutoutVeil } from "../interaction/veil.js";
13
+ import { resolvePalette } from "../runtime/categories.js";
14
+ import { addOverlayText, drawCategoricalLegend, drawTitleBlock } from "./chrome.js";
15
+ import { drawColorRampLegend } from "./colorRamp.js";
16
+ import { rowAlpha } from "./emphasis.js";
17
+ import { albersParallels, centralMeridianOf, fitBounds, polygonsOf, projector, wrapDegrees } from "./geo.js";
18
+ import { compositeOver, contrastTarget, inkOn, knownBackdrop, toneForContrast } from "./legibility.js";
19
+ import { lerp, mixColor, mixFill, morphInput, morphKeyed, rampAt, stashMorphFrame, switchAt } from "./morphFrame.js";
20
+ import { drawNoData } from "./noData.js";
21
+ const TAU = Math.PI * 2;
22
+ const bubbleAlpha = (tokens) => tokens.dark ? 0.8 : 0.7;
23
+ const MIN_RADIUS = 2;
24
+ const HOLLOW_RADIUS = 3;
25
+ const PALE_CONTRAST = 1.6;
26
+ const RING_CONTRAST = 3;
27
+ const HIT_SLOP = 6;
28
+ const ENTRANCE_STAGGER = 0.45;
29
+ const MAX_LABELS = 60;
30
+ const LABEL_AREA = 14e3;
31
+ const LABEL_GAP = 3;
32
+ const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
33
+ function numeric(value) {
34
+ return typeof value === "number" || typeof value === "string" ? toNumber(value) : NaN;
35
+ }
36
+ function fieldOf(def) {
37
+ const field = def?.field;
38
+ return typeof field === "string" && field !== "" ? field : void 0;
39
+ }
40
+ function insetRect(rect, pad) {
41
+ const p = Math.max(0, Math.min(pad, rect.width / 4, rect.height / 4));
42
+ return { x: rect.x + p, y: rect.y + p, width: rect.width - p * 2, height: rect.height - p * 2 };
43
+ }
44
+ function applyPadding(rect, padding) {
45
+ if (!padding || typeof padding !== "object") return rect;
46
+ const p = padding;
47
+ const side = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
48
+ const [top, right, bottom, left] = [side(p.top), side(p.right), side(p.bottom), side(p.left)];
49
+ const out = { x: rect.x + left, y: rect.y + top, width: rect.width - left - right, height: rect.height - top - bottom };
50
+ return out.width >= 8 && out.height >= 8 ? out : rect;
51
+ }
52
+ function readObservations(map, planar) {
53
+ const rows = Array.isArray(map.data) ? map.data : [];
54
+ const lonField = fieldOf(map.encoding?.longitude);
55
+ const latField = fieldOf(map.encoding?.latitude);
56
+ if (!lonField || !latField) return [];
57
+ const readLon = accessor(lonField);
58
+ const readLat = accessor(latField);
59
+ const sizeField = fieldOf(map.encoding?.size);
60
+ const readSize = sizeField ? accessor(sizeField) : null;
61
+ const out = [];
62
+ rows.forEach((row, index) => {
63
+ if (row == null || typeof row !== "object") return;
64
+ const lon = numeric(readLon(row));
65
+ const lat = numeric(readLat(row));
66
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) return;
67
+ if (!planar && (Math.abs(lon) > 180 || Math.abs(lat) > 90)) return;
68
+ let size = null;
69
+ if (readSize) {
70
+ size = numeric(readSize(row));
71
+ if (!Number.isFinite(size)) return;
72
+ }
73
+ out.push({ row, index, lon, lat, size });
74
+ });
75
+ return out;
76
+ }
77
+ function basemapPolygons(geo, planar) {
78
+ const features = geo && Array.isArray(geo.features) ? geo.features : [];
79
+ const out = [];
80
+ for (const feature of features) {
81
+ if (!feature || typeof feature !== "object") continue;
82
+ const parts = polygonsOf(feature);
83
+ if (!Array.isArray(parts)) continue;
84
+ for (const polygon of parts) {
85
+ if (!Array.isArray(polygon)) continue;
86
+ const rings = [];
87
+ for (const ring of polygon) {
88
+ if (!Array.isArray(ring)) continue;
89
+ const points = [];
90
+ for (const pos of ring) {
91
+ if (!Array.isArray(pos)) continue;
92
+ const [x, y] = pos;
93
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) continue;
94
+ if (!planar && (Math.abs(x) > 360 || Math.abs(y) > 90)) continue;
95
+ points.push([x, y]);
96
+ }
97
+ if (points.length) rings.push(points);
98
+ }
99
+ if (rings.length) out.push(rings);
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ function radiusLimits(map, mapRect) {
105
+ const extent = Math.max(0, Math.min(mapRect.width, mapRect.height));
106
+ const ceiling = Math.max(4, extent / 2);
107
+ const requested = typeof map.maxRadius === "number" && Number.isFinite(map.maxRadius) && map.maxRadius > 0 ? Math.min(map.maxRadius, ceiling) : null;
108
+ const rMax = requested ?? clamp(extent * 0.06, 4, 40);
109
+ return { rMax, rMin: Math.min(MIN_RADIUS, rMax), dot: requested ?? clamp(extent * 0.012, 4, 6) };
110
+ }
111
+ function paletteOf(map, tokens) {
112
+ const valid = (colors) => Array.isArray(colors) ? colors.filter((c) => typeof c === "string" && parseColor(c) != null) : [];
113
+ const resolved = valid(resolvePalette(map.palette, tokens));
114
+ if (resolved.length) return resolved;
115
+ const theme = valid(tokens.color.palette);
116
+ return theme.length ? theme : ["#3b82f6"];
117
+ }
118
+ function categoryLabel(value, format) {
119
+ const key = toKey(value);
120
+ return key === "" ? "(blank)" : formatValue(value, format) || key;
121
+ }
122
+ function resolveColors(map, observations, tokens) {
123
+ const palette = paletteOf(map, tokens);
124
+ const scheme = map.scheme ?? "teal";
125
+ const field = fieldOf(map.encoding?.color);
126
+ if (!field) {
127
+ const fill = palette[0];
128
+ return { mode: "none", domain: null, scheme, entries: [], fillOf: () => ({ fill, value: void 0, noData: false }) };
129
+ }
130
+ const read = accessor(field);
131
+ const rows = observations.map((o) => o.row);
132
+ const declared = map.encoding.color?.type;
133
+ if ((declared ?? inferType(rows, field)) === "quantitative") {
134
+ let min = Infinity;
135
+ let max = -Infinity;
136
+ for (const row of rows) {
137
+ const v = numeric(read(row));
138
+ if (!Number.isFinite(v)) continue;
139
+ if (v < min) min = v;
140
+ if (v > max) max = v;
141
+ }
142
+ const domain = min <= max ? [min, max] : null;
143
+ const scale2 = domain && sequentialColorScale({ domain, interpolator: sequential(scheme) });
144
+ const noData = tokens.color.textMuted;
145
+ return {
146
+ mode: "numeric",
147
+ domain,
148
+ scheme,
149
+ entries: [],
150
+ fillOf: (row) => {
151
+ const raw = read(row);
152
+ const v = numeric(raw);
153
+ if (!scale2 || !domain || !Number.isFinite(v)) return { fill: noData, value: raw, noData: true };
154
+ return { fill: rgbaToCss(scale2.map(v)), ramp: { scheme, at: rampAt(domain, v) }, value: v, noData: false };
155
+ }
156
+ };
157
+ }
158
+ const keys = [];
159
+ const raws = /* @__PURE__ */ new Map();
160
+ for (const row of rows) {
161
+ const raw = read(row);
162
+ const key = toKey(raw);
163
+ if (!raws.has(key)) {
164
+ raws.set(key, raw);
165
+ keys.push(key);
166
+ }
167
+ }
168
+ const scale = ordinalColorScale({ domain: keys, palette });
169
+ const format = map.encoding.color?.format;
170
+ return {
171
+ mode: "categorical",
172
+ domain: null,
173
+ scheme,
174
+ entries: keys.map((key) => ({ label: categoryLabel(raws.get(key), format), color: rgbaToCss(scale.map(key)), symbol: "circle" })),
175
+ fillOf: (row) => {
176
+ const raw = read(row);
177
+ return { fill: rgbaToCss(scale.map(toKey(raw))), value: raw, noData: false };
178
+ }
179
+ };
180
+ }
181
+ function legendVisible(legend) {
182
+ if (legend === false) return false;
183
+ return !(legend && typeof legend === "object" && legend.show === false);
184
+ }
185
+ function legendPosition(map, area, bounds) {
186
+ const requested = map.legend && typeof map.legend === "object" ? map.legend.position : void 0;
187
+ if (requested === "top" || requested === "right" || requested === "bottom" || requested === "left") return requested;
188
+ if (!(area.width > 0) || !(area.height > 0)) return "bottom";
189
+ const spanX = bounds ? bounds.maxX - bounds.minX : 0;
190
+ const spanY = bounds ? bounds.maxY - bounds.minY : 0;
191
+ const mapAspect = spanX > 0 && spanY > 0 ? spanX / spanY : 1.5;
192
+ return mapAspect > area.width / area.height ? "bottom" : "right";
193
+ }
194
+ function boundsOf(points) {
195
+ let minX = Infinity;
196
+ let maxX = -Infinity;
197
+ let minY = Infinity;
198
+ let maxY = -Infinity;
199
+ for (const [x, y] of points) {
200
+ if (x < minX) minX = x;
201
+ if (x > maxX) maxX = x;
202
+ if (y < minY) minY = y;
203
+ if (y > maxY) maxY = y;
204
+ }
205
+ return { minX, maxX, minY, maxY };
206
+ }
207
+ function pointBounds(projected, observations, project, planar) {
208
+ const b = boundsOf(projected);
209
+ const tolerance = 1e-9 * Math.max(1, Math.abs(b.minX), Math.abs(b.maxX), Math.abs(b.minY), Math.abs(b.maxY));
210
+ if (b.maxX - b.minX > tolerance || b.maxY - b.minY > tolerance) return b;
211
+ let halfX = 1;
212
+ let halfY = 1;
213
+ if (!planar) {
214
+ const { lon, lat } = observations[0];
215
+ const [x0, y0] = project([lon - 1, clamp(lat - 1, -89, 89)]);
216
+ const [x1, y1] = project([lon + 1, clamp(lat + 1, -89, 89)]);
217
+ halfX = Math.abs(x1 - x0) / 2 || 1;
218
+ halfY = Math.abs(y1 - y0) / 2 || 1;
219
+ }
220
+ const cx = (b.minX + b.maxX) / 2;
221
+ const cy = (b.minY + b.maxY) / 2;
222
+ return { minX: cx - halfX, maxX: cx + halfX, minY: cy - halfY, maxY: cy + halfY };
223
+ }
224
+ function resolveProjection(kind, basemap, observations) {
225
+ const planar = kind === "identity";
226
+ const cone = (lats) => {
227
+ let lo = Infinity;
228
+ let hi = -Infinity;
229
+ for (const lat of lats) {
230
+ if (lat < lo) lo = lat;
231
+ if (lat > hi) hi = lat;
232
+ }
233
+ return lo <= hi ? albersParallels([lo, hi]) : void 0;
234
+ };
235
+ if (basemap.length) {
236
+ const lons = [];
237
+ const lats = [];
238
+ for (const polygon of basemap) {
239
+ for (const ring of polygon) {
240
+ for (const [x, y] of ring) {
241
+ lons.push(x);
242
+ lats.push(y);
243
+ }
244
+ }
245
+ }
246
+ const lon02 = planar ? 0 : centralMeridianOf(lons);
247
+ const project2 = projector(kind, lon02, kind === "albers" ? cone(lats) : void 0);
248
+ const projected = [];
249
+ const all = [];
250
+ for (const polygon of basemap) {
251
+ const rings = [];
252
+ for (const ring of polygon) {
253
+ const pts = [];
254
+ for (const pos of ring) {
255
+ const p = project2(pos);
256
+ if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
257
+ pts.push(p);
258
+ all.push(p);
259
+ }
260
+ if (pts.length >= 3) rings.push(pts);
261
+ }
262
+ if (rings.length) projected.push(rings);
263
+ }
264
+ const bounds = boundsOf(all);
265
+ if (projected.length && bounds.maxX - bounds.minX > 0 && bounds.maxY - bounds.minY > 0) {
266
+ return { project: project2, lon0: lon02, basemap: projected, bounds };
267
+ }
268
+ }
269
+ const lon0 = planar ? 0 : centralMeridianOf(observations.map((o) => o.lon));
270
+ const project = projector(kind, lon0, kind === "albers" ? cone(observations.map((o) => o.lat)) : void 0);
271
+ return { project, lon0, basemap: null, bounds: null };
272
+ }
273
+ const GRATICULE_STEPS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30];
274
+ function graticuleLines(kind, project, fit, observations, lon0) {
275
+ if (kind === "identity" || observations.length === 0) return [];
276
+ let relMin = Infinity;
277
+ let relMax = -Infinity;
278
+ let latMin = Infinity;
279
+ let latMax = -Infinity;
280
+ for (const o of observations) {
281
+ const d = wrapDegrees(o.lon - lon0);
282
+ relMin = Math.min(relMin, d);
283
+ relMax = Math.max(relMax, d);
284
+ latMin = Math.min(latMin, o.lat);
285
+ latMax = Math.max(latMax, o.lat);
286
+ }
287
+ const span = Math.max(relMax - relMin, latMax - latMin, 2);
288
+ const step = GRATICULE_STEPS.find((s) => span / s <= 10) ?? 30;
289
+ const latLimit = kind === "mercator" ? 80 : 90;
290
+ const lonLo = lon0 + Math.max(-180, relMin - span * 3);
291
+ const lonHi = lon0 + Math.min(180, relMax + span * 3);
292
+ const latLo = Math.max(-latLimit, latMin - span * 3);
293
+ const latHi = Math.min(latLimit, latMax + span * 3);
294
+ const toScreen = (lon, lat) => {
295
+ const [x, y] = project([lon, lat]);
296
+ const sx = fit.x(x);
297
+ const sy = fit.y(y);
298
+ return Number.isFinite(sx) && Number.isFinite(sy) ? [sx, sy] : null;
299
+ };
300
+ const lines = [];
301
+ const add = (points) => {
302
+ const line = points.filter((p) => p != null);
303
+ if (line.length >= 2) lines.push(line);
304
+ };
305
+ const arcSteps = kind === "albers" ? 48 : 1;
306
+ for (let k = Math.ceil(lonLo / step); k * step <= lonHi && lines.length < 200; k += 1) {
307
+ add([toScreen(k * step, latLo), toScreen(k * step, latHi)]);
308
+ }
309
+ for (let k = Math.ceil(latLo / step); k * step <= latHi && lines.length < 400; k += 1) {
310
+ const lat = k * step;
311
+ add(Array.from({ length: arcSteps + 1 }, (_, i) => toScreen(lonLo + (lonHi - lonLo) * i / arcSteps, lat)));
312
+ }
313
+ return lines;
314
+ }
315
+ function paintGraticule(surface, tokens, lines, clip, pen) {
316
+ if (!lines.length) return;
317
+ const ctx = surface.marks.ctx;
318
+ ctx.save();
319
+ ctx.beginPath();
320
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
321
+ ctx.clip();
322
+ ctx.setLineDash([]);
323
+ ctx.globalAlpha = 1;
324
+ ctx.lineWidth = 1;
325
+ ctx.strokeStyle = tokens.color.grid;
326
+ for (const line of lines) {
327
+ if (pen) {
328
+ pen.polyline(line.map(([x, y]) => ({ x, y })), { stroke: tokens.color.grid, strokeWidth: 1 });
329
+ continue;
330
+ }
331
+ ctx.beginPath();
332
+ line.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
333
+ ctx.stroke();
334
+ }
335
+ ctx.restore();
336
+ }
337
+ function niceFloor(value) {
338
+ if (!(value > 0) || !Number.isFinite(value)) return 0;
339
+ const unit = 10 ** Math.floor(Math.log10(value));
340
+ const clean = (n) => Number(n.toPrecision(12));
341
+ let nice = clean(Math.floor(value / unit + 1e-9) * unit);
342
+ if (nice < value * 0.7) nice = clean(Math.floor(value * 10 / unit + 1e-9) * (unit / 10));
343
+ return nice > 0 ? nice : value;
344
+ }
345
+ function legendFormat(format, largest) {
346
+ if (largest < 1e4) return format ?? (largest >= 1e3 ? "," : void 0);
347
+ if (format == null) return "s";
348
+ const plain = /^(\$)?,?(\.\d+)?[fd]?$/.exec(format.trim());
349
+ return plain ? `${plain[1] ?? ""}s` : format;
350
+ }
351
+ function formatLocation(b, planar, map) {
352
+ const lonFormat = map.encoding?.longitude?.format;
353
+ const latFormat = map.encoding?.latitude?.format;
354
+ if (planar) return `${formatValue(b.lon, lonFormat)}, ${formatValue(b.lat, latFormat)}`;
355
+ const deg = (v, pos, neg, format) => {
356
+ const text = format ? formatValue(Math.abs(v), format) : formatValue(Math.round(Math.abs(v) * 100) / 100);
357
+ return `${text}\xB0${v < 0 ? neg : v > 0 ? pos : ""}`;
358
+ };
359
+ return `${deg(b.lat, "N", "S", latFormat)}, ${deg(b.lon, "E", "W", lonFormat)}`;
360
+ }
361
+ const BUBBLE_MORPH = {
362
+ lerp: (a, b, t) => ({
363
+ key: b.key,
364
+ x: lerp(a.x, b.x, t),
365
+ y: lerp(a.y, b.y, t),
366
+ r: lerp(a.r, b.r, t),
367
+ ...mixFill(a, b, t),
368
+ stroke: mixColor(a.stroke, b.stroke, t),
369
+ outlined: switchAt(a.outlined, b.outlined, t),
370
+ hollow: switchAt(a.hollow, b.hollow, t),
371
+ label: b.label,
372
+ emph: lerp(a.emph, b.emph, t),
373
+ alpha: lerp(a.alpha, b.alpha, t)
374
+ }),
375
+ enter: (b, t) => ({ ...b, r: b.r * t, alpha: t }),
376
+ exit: (a, t) => ({ ...a, r: a.r * (1 - t), alpha: a.alpha * (1 - t) })
377
+ };
378
+ const toMark = (b) => ({
379
+ key: b.key,
380
+ x: b.x,
381
+ y: b.y,
382
+ r: b.r,
383
+ fill: b.fill,
384
+ ramp: b.ramp,
385
+ stroke: b.stroke,
386
+ outlined: b.outlined,
387
+ hollow: b.hollow,
388
+ label: b.label,
389
+ emph: b.emph,
390
+ alpha: b.alpha
391
+ });
392
+ function paintBubbles(surface, marks, clip, pen, fillAlpha) {
393
+ const ctx = surface.marks.ctx;
394
+ ctx.save();
395
+ ctx.beginPath();
396
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
397
+ ctx.clip();
398
+ ctx.setLineDash([]);
399
+ for (const m of marks) {
400
+ const a = m.emph * m.alpha;
401
+ if (!(m.r > 0) || !(a > 0) || !Number.isFinite(m.x) || !Number.isFinite(m.y)) continue;
402
+ const dimmed = m.emph < 1;
403
+ if (pen) {
404
+ ctx.globalAlpha = a;
405
+ pen.circle(m.x, m.y, m.r, m.hollow ? { stroke: m.stroke, strokeWidth: 1.5 } : { fill: m.fill, fillAlpha, stroke: m.stroke, strokeWidth: 1 });
406
+ } else {
407
+ ctx.beginPath();
408
+ ctx.arc(m.x, m.y, m.r, 0, TAU);
409
+ if (m.hollow) {
410
+ ctx.globalAlpha = a;
411
+ ctx.strokeStyle = m.stroke;
412
+ ctx.lineWidth = 1.5;
413
+ ctx.stroke();
414
+ } else {
415
+ ctx.globalAlpha = fillAlpha * a;
416
+ ctx.fillStyle = m.fill;
417
+ ctx.fill();
418
+ ctx.globalAlpha = a;
419
+ ctx.strokeStyle = m.stroke;
420
+ ctx.lineWidth = 1;
421
+ ctx.stroke();
422
+ }
423
+ }
424
+ if (m.hollow) {
425
+ surface.evidence?.mark("point", m.x - m.r - 0.75, m.y - m.r - 0.75, m.r * 2 + 1.5, m.r * 2 + 1.5, m.stroke, a, clip, 1, void 0, dimmed);
426
+ } else {
427
+ const boundary = m.outlined ? { color: m.stroke, opacity: a, width: 1 } : void 0;
428
+ surface.evidence?.mark("point", m.x - m.r, m.y - m.r, m.r * 2, m.r * 2, m.fill, fillAlpha * a, clip, 1, void 0, dimmed, boundary);
429
+ }
430
+ }
431
+ ctx.globalAlpha = 1;
432
+ ctx.restore();
433
+ }
434
+ function paintBasemap(surface, tokens, polygons, bg, pen) {
435
+ const ctx = surface.marks.ctx;
436
+ ctx.save();
437
+ ctx.lineJoin = "round";
438
+ ctx.setLineDash([]);
439
+ ctx.globalAlpha = 1;
440
+ ctx.fillStyle = withAlpha(tokens.color.textMuted, tokens.dark ? 0.2 : 0.13);
441
+ ctx.strokeStyle = bg;
442
+ ctx.lineWidth = 0.8;
443
+ for (const polygon of polygons) {
444
+ ctx.beginPath();
445
+ for (const ring of polygon) {
446
+ ring.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
447
+ ctx.closePath();
448
+ }
449
+ ctx.fill("evenodd");
450
+ if (!pen) ctx.stroke();
451
+ }
452
+ if (pen) {
453
+ const stroke = withAlpha(tokens.color.textMuted, 0.45);
454
+ for (const polygon of polygons) {
455
+ for (const ring of polygon) pen.polygon(ring.map(([x, y]) => ({ x, y })), { stroke, strokeWidth: 0.8 });
456
+ }
457
+ }
458
+ ctx.restore();
459
+ }
460
+ function drawLeavingBubbles(surface, clip, fillAlpha, context) {
461
+ const morph = morphInput(context);
462
+ const leaving = morph && Array.isArray(morph.from) ? morphKeyed(morph.from, [], morph.t, BUBBLE_MORPH).exits : [];
463
+ stashMorphFrame(surface, "bubbleMap", leaving);
464
+ if (leaving.length) paintBubbles(surface, leaving, clip, null, fillAlpha);
465
+ }
466
+ function entranceMarks(marks, t) {
467
+ const n = marks.length;
468
+ const rank = new Array(n);
469
+ marks.map((_, i) => i).sort((a, b) => marks[a].x - marks[b].x || a - b).forEach((index, position) => rank[index] = position);
470
+ const stagger = n >= 3 ? ENTRANCE_STAGGER : 0;
471
+ return marks.map((m, i) => ({ ...m, r: m.r * Math.max(0, staggerProgress(t, rank[i], n, stagger, backOut)) }));
472
+ }
473
+ function intersectArea(a, b) {
474
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
475
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
476
+ return w > 0 && h > 0 ? w * h : 0;
477
+ }
478
+ function pointInRing(x, y, ring) {
479
+ let inside = false;
480
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
481
+ const [xi, yi] = ring[i];
482
+ const [xj, yj] = ring[j];
483
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
484
+ }
485
+ return inside;
486
+ }
487
+ function landShare(rect, polygons) {
488
+ if (!polygons?.length) return 0;
489
+ let hits = 0;
490
+ let total = 0;
491
+ for (let i = 0; i < 4; i += 1) {
492
+ for (let j = 0; j < 3; j += 1) {
493
+ const x = rect.x + (i + 0.5) / 4 * rect.width;
494
+ const y = rect.y + (j + 0.5) / 3 * rect.height;
495
+ total += 1;
496
+ if (polygons.some((polygon) => polygon.length > 0 && pointInRing(x, y, polygon[0]) && !polygon.slice(1).some((hole) => pointInRing(x, y, hole)))) hits += 1;
497
+ }
498
+ }
499
+ return hits / total;
500
+ }
501
+ function drawSizeLegend(surface, tokens, map, args) {
502
+ const { mapRect, bubbles, sMax, limits, basemap, bg, pen } = args;
503
+ const sizeDef = map.encoding?.size;
504
+ const sizeField = fieldOf(sizeDef);
505
+ if (!sizeField || !(sMax > 0) || limits.rMax < 6 || mapRect.width < 260 || mapRect.height < 150) return null;
506
+ const labelSize = tokens.font.size.tiny;
507
+ const labelH = labelSize * 1.2;
508
+ const radius = (v) => Math.max(limits.rMin, limits.rMax * Math.sqrt(v / sMax));
509
+ const refs = [];
510
+ for (let v = niceFloor(sMax); refs.length < 3 && v > 0; v = niceFloor(v / 4)) {
511
+ const r = radius(v);
512
+ const prev = refs[refs.length - 1];
513
+ if (prev && (2 * (prev.r - r) < labelH + 1 || r < 3)) break;
514
+ refs.push({ value: v, r });
515
+ }
516
+ if (!refs.length) return null;
517
+ const texts = refs.map((ref) => formatValue(ref.value, legendFormat(sizeDef?.format, refs[0].value)));
518
+ const font = fontString(labelSize, tokens.font.family, tokens.font.weight.normal);
519
+ const titleFont = fontString(labelSize, tokens.font.family, tokens.font.weight.medium);
520
+ const title = sizeDef?.title ?? sizeField;
521
+ const r1 = refs[0].r;
522
+ const padX = 8;
523
+ const padY = 6;
524
+ const gap = 8;
525
+ const labelW = Math.max(...texts.map((text) => measureText(text, font).width));
526
+ const width = Math.min(180, Math.max(r1 * 2 + gap + labelW, measureText(title, titleFont).width) + padX * 2);
527
+ const titleH = labelH * 1.5 + 2;
528
+ const height = padY * 2 + titleH + r1 * 2;
529
+ if (width > mapRect.width * 0.45 || height > mapRect.height * 0.6) return null;
530
+ const m = 6;
531
+ const corners = [
532
+ { x: mapRect.x + m, y: mapRect.y + mapRect.height - m - height, width, height },
533
+ { x: mapRect.x + mapRect.width - m - width, y: mapRect.y + mapRect.height - m - height, width, height },
534
+ { x: mapRect.x + m, y: mapRect.y + m, width, height },
535
+ { x: mapRect.x + mapRect.width - m - width, y: mapRect.y + m, width, height }
536
+ ];
537
+ let best = null;
538
+ let bestScore = Infinity;
539
+ let bestOverlap = 0;
540
+ for (const rect of corners) {
541
+ let overlap = 0;
542
+ for (const b of bubbles) overlap += intersectArea(rect, { x: b.x - b.r, y: b.y - b.r, width: b.r * 2, height: b.r * 2 });
543
+ const score = overlap + landShare(rect, basemap) * width * height * 0.25;
544
+ if (score < bestScore) {
545
+ best = rect;
546
+ bestScore = score;
547
+ bestOverlap = overlap;
548
+ }
549
+ }
550
+ if (!best || bestOverlap > width * height * 0.08) return null;
551
+ const ctx = surface.marks.ctx;
552
+ const muted = tokens.color.textMuted;
553
+ ctx.save();
554
+ ctx.globalAlpha = 1;
555
+ ctx.setLineDash([]);
556
+ ctx.beginPath();
557
+ roundedRect(ctx, best.x, best.y, best.width, best.height, 6);
558
+ ctx.fillStyle = withAlpha(bg, 0.88);
559
+ ctx.fill();
560
+ ctx.lineWidth = 1;
561
+ ctx.strokeStyle = tokens.color.border;
562
+ ctx.stroke();
563
+ const cx = best.x + padX + r1;
564
+ const baseY = best.y + best.height - padY;
565
+ const labelX = best.x + padX + r1 * 2 + gap;
566
+ refs.forEach(({ r }) => {
567
+ if (pen) {
568
+ pen.circle(cx, baseY - r, r, { stroke: muted, strokeWidth: 1 });
569
+ } else {
570
+ ctx.beginPath();
571
+ ctx.arc(cx, baseY - r, r, 0, TAU);
572
+ ctx.fillStyle = withAlpha(muted, 0.06);
573
+ ctx.fill();
574
+ ctx.strokeStyle = muted;
575
+ ctx.lineWidth = 1;
576
+ ctx.stroke();
577
+ }
578
+ ctx.beginPath();
579
+ ctx.moveTo(cx, baseY - r * 2);
580
+ ctx.lineTo(labelX - 3, baseY - r * 2);
581
+ ctx.globalAlpha = 0.6;
582
+ ctx.strokeStyle = muted;
583
+ ctx.lineWidth = 0.75;
584
+ ctx.stroke();
585
+ ctx.globalAlpha = 1;
586
+ });
587
+ ctx.restore();
588
+ addOverlayText(surface, tokens, {
589
+ left: best.x + padX,
590
+ top: best.y + padY,
591
+ text: title,
592
+ maxWidth: best.width - padX * 2,
593
+ color: muted,
594
+ size: labelSize,
595
+ weight: tokens.font.weight.medium,
596
+ role: "legend",
597
+ path: "/encoding/size"
598
+ });
599
+ refs.forEach(({ r }, i) => {
600
+ addOverlayText(surface, tokens, {
601
+ left: labelX,
602
+ top: baseY - r * 2 - labelH / 2,
603
+ text: texts[i],
604
+ color: muted,
605
+ size: labelSize,
606
+ role: "legend",
607
+ path: "/encoding/size"
608
+ });
609
+ });
610
+ return best;
611
+ }
612
+ function addHaloLabel(surface, tokens, o) {
613
+ if (o.halo && surface.headless) {
614
+ const font = fontString(o.size, tokens.font.family, o.weight);
615
+ const fitted = fitLabel(o.text, o.maxWidth, font);
616
+ const cmd = overlayTextToCanvasCmd({ left: o.left, top: o.top, text: fitted.text, color: o.color, size: o.size }, font);
617
+ const ctx = surface.marks.ctx;
618
+ ctx.save();
619
+ ctx.globalAlpha = 0.9 * o.opacity;
620
+ ctx.font = font;
621
+ ctx.textAlign = cmd.align ?? "left";
622
+ ctx.textBaseline = cmd.baseline ?? "top";
623
+ ctx.lineJoin = "round";
624
+ ctx.lineWidth = 3;
625
+ ctx.strokeStyle = o.halo;
626
+ ctx.strokeText(fitted.text, cmd.x, cmd.y);
627
+ ctx.restore();
628
+ }
629
+ const el = addOverlayText(surface, tokens, {
630
+ left: o.left,
631
+ top: o.top,
632
+ text: o.text,
633
+ maxWidth: o.maxWidth,
634
+ color: o.color,
635
+ size: o.size,
636
+ weight: o.weight,
637
+ opacity: o.opacity < 1 ? o.opacity : void 0,
638
+ role: "data-label",
639
+ background: o.background,
640
+ path: "/labels"
641
+ });
642
+ if (o.halo && el) el.style.textShadow = `0 0 1px ${o.halo}, 0 0 2px ${o.halo}, 0 0 3px ${o.halo}`;
643
+ }
644
+ function drawBubbleLabels(surface, tokens, args) {
645
+ const { marks, mapRect, obstacles, opacity, bg, backdrop, fillAlpha } = args;
646
+ const size = tokens.font.size.small;
647
+ const weight = tokens.font.weight.medium;
648
+ const font = fontString(size, tokens.font.family, weight);
649
+ const h = size * 1.2;
650
+ const maxWidth = Math.max(24, Math.min(160, mapRect.width * 0.4));
651
+ const bounds = { x: mapRect.x + 1, y: mapRect.y + 1, width: mapRect.width - 2, height: mapRect.height - 2 };
652
+ const within = (box) => box.x >= bounds.x && box.y >= bounds.y && box.x + box.width <= bounds.x + bounds.width && box.y + box.height <= bounds.y + bounds.height;
653
+ const placed = [...obstacles];
654
+ const omit = (reason, example) => surface.labels?.omit({ role: "data-label", reason, required: false, example, path: "/labels" });
655
+ const budget = Math.max(3, Math.min(MAX_LABELS, Math.round(mapRect.width * mapRect.height / LABEL_AREA)));
656
+ const covers = (box, self) => marks.some((o, j) => {
657
+ if (j === self || !(o.r > 0)) return false;
658
+ const dx = Math.max(box.x - o.x, 0, o.x - (box.x + box.width));
659
+ const dy = Math.max(box.y - o.y, 0, o.y - (box.y + box.height));
660
+ return dx * dx + dy * dy < Math.max(0, o.r - 1) ** 2 || dx === 0 && dy === 0;
661
+ });
662
+ const order = marks.map((_, i) => i).filter((i) => marks[i].label !== "" && marks[i].r > 0 && marks[i].emph * marks[i].alpha >= 0.5).sort((a, b) => marks[b].r - marks[a].r || a - b);
663
+ order.forEach((index, rank) => {
664
+ const m = marks[index];
665
+ if (rank >= budget) return omit("space", m.label);
666
+ const fitted = fitLabel(m.label, maxWidth, font);
667
+ if (!fitted.text || fitted.truncated && fitted.text.length < m.label.length * 0.5) return omit("unreadable", m.label);
668
+ const w = measureText(fitted.text, font).width;
669
+ const spots = [];
670
+ if (!m.hollow && Math.hypot(w / 2, h / 2) <= m.r - 1.5) spots.push({ x: m.x - w / 2, y: m.y - h / 2, inside: true });
671
+ spots.push(
672
+ { x: m.x + m.r + LABEL_GAP, y: m.y - h / 2, inside: false },
673
+ { x: m.x - m.r - LABEL_GAP - w, y: m.y - h / 2, inside: false },
674
+ { x: m.x - w / 2, y: m.y - m.r - LABEL_GAP + 1 - h, inside: false },
675
+ { x: m.x - w / 2, y: m.y + m.r + LABEL_GAP - 1, inside: false }
676
+ );
677
+ const spot = spots.find((s) => {
678
+ const box = { x: s.x, y: s.y, width: w, height: h };
679
+ return within(box) && !placed.some((p) => boxesOverlap(p, box, -1)) && !covers(box, index);
680
+ });
681
+ if (!spot) return omit("collision", m.label);
682
+ placed.push({ x: spot.x, y: spot.y, width: w, height: h });
683
+ let color = tokens.color.text;
684
+ let background = bg;
685
+ if (spot.inside) {
686
+ const fill = parseColor(m.fill);
687
+ const under = fill && backdrop ? compositeOver(fill, fillAlpha * m.emph * m.alpha, backdrop) : null;
688
+ if (under) {
689
+ color = inkOn(under, tokens);
690
+ background = rgbaToCss(under);
691
+ }
692
+ }
693
+ addHaloLabel(surface, tokens, {
694
+ left: spot.x,
695
+ top: spot.y,
696
+ text: m.label,
697
+ maxWidth,
698
+ color,
699
+ size,
700
+ weight,
701
+ opacity,
702
+ halo: spot.inside ? null : bg,
703
+ background
704
+ });
705
+ });
706
+ }
707
+ function tooltipFor(map, b, planar) {
708
+ const enc = map.encoding;
709
+ const location = formatLocation(b, planar, map);
710
+ const rows = [];
711
+ const sizeField = fieldOf(enc.size);
712
+ const colorField = fieldOf(enc.color);
713
+ if (sizeField && b.size != null) {
714
+ const value = formatValue(b.size, enc.size?.format);
715
+ rows.push({ swatch: b.hollow ? b.stroke : b.fill, label: enc.size?.title ?? sizeField, value: b.hollow ? `${value} (not sized)` : value });
716
+ }
717
+ if (colorField && colorField !== sizeField) {
718
+ rows.push({
719
+ swatch: rows.length ? void 0 : b.fill,
720
+ label: enc.color?.title ?? colorField,
721
+ value: b.colorNoData ? "no data" : formatValue(b.colorValue, enc.color?.format)
722
+ });
723
+ }
724
+ if (b.label) return { title: b.label, rows: [...rows, { label: "location", value: location, muted: true }] };
725
+ if (rows.length) return { title: location, rows };
726
+ return { rows: [{ swatch: b.fill, label: "location", value: location }] };
727
+ }
728
+ function pickFor(map, b) {
729
+ const labelField = fieldOf(map.encoding?.label);
730
+ if (labelField && toKey(b.rawLabel) !== "") return { kind: "point", fields: [labelField], tuples: [[b.rawLabel]] };
731
+ const lonField = fieldOf(map.encoding?.longitude);
732
+ const latField = fieldOf(map.encoding?.latitude);
733
+ if (!lonField || !latField) return null;
734
+ return { kind: "point", fields: [lonField, latField], tuples: [[accessor(lonField)(b.row), accessor(latField)(b.row)]] };
735
+ }
736
+ function buildInteraction(map, tokens, mapRect, bubbles, bg, planar) {
737
+ const tt = map.tooltip;
738
+ const tooltipOn = !(tt === false || tt && typeof tt === "object" && tt.show === false);
739
+ const focusOn = hoverFocusEnabled(map);
740
+ const hit = (px, py) => {
741
+ for (let i = bubbles.length - 1; i >= 0; i -= 1) {
742
+ const b = bubbles[i];
743
+ const dx = px - b.x;
744
+ const dy = py - b.y;
745
+ if (dx * dx + dy * dy <= b.r * b.r) return b;
746
+ }
747
+ let best = null;
748
+ let bestGap = HIT_SLOP;
749
+ for (const b of bubbles) {
750
+ const gap = Math.hypot(px - b.x, py - b.y) - b.r;
751
+ if (gap <= bestGap) {
752
+ best = b;
753
+ bestGap = gap;
754
+ }
755
+ }
756
+ return best;
757
+ };
758
+ const paintHover = (ictx, b, strength) => {
759
+ if (focusOn) {
760
+ paintCutoutVeil(ictx, mapRect, bg, FOCUS_VEIL * strength, (c) => {
761
+ c.moveTo(b.x + b.r + 1, b.y);
762
+ c.arc(b.x, b.y, b.r + 1, 0, TAU);
763
+ });
764
+ }
765
+ ictx.save();
766
+ ictx.globalAlpha = strength;
767
+ for (const [width, color] of [[3, bg], [1.5, tokens.color.text]]) {
768
+ ictx.beginPath();
769
+ ictx.arc(b.x, b.y, b.r + 1.5, 0, TAU);
770
+ ictx.lineWidth = width;
771
+ ictx.strokeStyle = color;
772
+ ictx.stroke();
773
+ }
774
+ ictx.restore();
775
+ };
776
+ return {
777
+ region: mapRect,
778
+ hitTest: (px, py) => {
779
+ if (!tooltipOn) return null;
780
+ const b = hit(px, py);
781
+ if (!b) return null;
782
+ return {
783
+ key: String(b.index),
784
+ anchorX: b.x,
785
+ anchorY: b.y,
786
+ content: tooltipFor(map, b, planar),
787
+ paint: (ictx, state) => paintHover(ictx, b, state.focus),
788
+ draw: (ictx) => paintHover(ictx, b, 1)
789
+ };
790
+ },
791
+ pick: (px, py) => {
792
+ const b = hit(px, py);
793
+ return b ? pickFor(map, b) : null;
794
+ }
795
+ };
796
+ }
797
+ function drawBubbleMap(surface, spec, tokens, size, context) {
798
+ surface.evidence?.begin("rows");
799
+ surface.evidence?.data(0);
800
+ const map = spec;
801
+ const content = drawTitleBlock(surface, tokens, size, map.title);
802
+ const kind = map.projection ?? "mercator";
803
+ const planar = kind === "identity";
804
+ const observations = readObservations(map, planar);
805
+ const bg = map.background ?? tokens.color.background;
806
+ const fillAlpha = bubbleAlpha(tokens);
807
+ if (content.width <= 0 || content.height <= 0) {
808
+ drawLeavingBubbles(surface, content, fillAlpha, context);
809
+ drawNoData(surface, tokens, content);
810
+ return;
811
+ }
812
+ const projection = resolveProjection(kind, basemapPolygons(map.geo, planar), observations);
813
+ const projected = observations.map((o) => projection.project([o.lon, o.lat]));
814
+ const bounds = projection.bounds ?? (projected.length ? pointBounds(projected, observations, projection.project, planar) : null);
815
+ const showLegend = legendVisible(map.legend);
816
+ const colors = resolveColors(map, observations, tokens);
817
+ let area = content;
818
+ let rampRect = null;
819
+ if (showLegend && colors.mode === "numeric" && colors.domain) {
820
+ const legendHeight = Math.ceil(tokens.font.size.small + 26);
821
+ area = { ...content, height: Math.max(10, content.height - legendHeight) };
822
+ rampRect = { x: content.x, y: area.y + area.height + 4, width: content.width, height: legendHeight };
823
+ } else if (showLegend && colors.mode === "categorical" && colors.entries.length) {
824
+ area = drawCategoricalLegend(surface, tokens, content, colors.entries, legendPosition(map, content, bounds));
825
+ }
826
+ const mapRect = applyPadding(area, map.padding);
827
+ const limits = radiusLimits(map, mapRect);
828
+ const sized = fieldOf(map.encoding?.size) != null;
829
+ const reach = sized ? limits.rMax : limits.dot;
830
+ const fitRect = insetRect(mapRect, (projection.basemap ? reach / 2 : reach) + 1);
831
+ if (!bounds) {
832
+ drawLeavingBubbles(surface, mapRect, fillAlpha, context);
833
+ if (rampRect) drawRamp(surface, tokens, map, colors, rampRect);
834
+ drawNoData(surface, tokens, content);
835
+ return;
836
+ }
837
+ const fit = fitBounds(bounds, fitRect, !planar);
838
+ const basemap = projection.basemap?.map((polygon) => polygon.map((ring) => ring.map(([x, y]) => [fit.x(x), fit.y(y)]))) ?? null;
839
+ const placed = [];
840
+ observations.forEach((o, i) => {
841
+ const x = fit.x(projected[i][0]);
842
+ const y = fit.y(projected[i][1]);
843
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
844
+ if (x < mapRect.x || x > mapRect.x + mapRect.width || y < mapRect.y || y > mapRect.y + mapRect.height) return;
845
+ placed.push({ o, x, y });
846
+ });
847
+ let sMax = 0;
848
+ for (const { o } of placed) if (o.size != null && o.size > sMax) sMax = o.size;
849
+ const radiusOf = (s) => {
850
+ if (!sized || s == null) return limits.dot;
851
+ if (s < 0) return Math.min(HOLLOW_RADIUS, limits.rMax);
852
+ if (!(sMax > 0)) return limits.rMin;
853
+ return Math.max(limits.rMin, limits.rMax * Math.sqrt(s / sMax));
854
+ };
855
+ const backdrop = knownBackdrop(surface, tokens);
856
+ const target = contrastTarget(backdrop, tokens);
857
+ const tones = /* @__PURE__ */ new Map();
858
+ const toneOf = (fill) => {
859
+ let tone = tones.get(fill);
860
+ if (!tone) {
861
+ const rgba = parseColor(fill);
862
+ const faint = rgba && backdrop && contrastRatio(compositeOver(rgba, fillAlpha, backdrop), backdrop) < PALE_CONTRAST;
863
+ tone = {
864
+ pale: faint ? toneForContrast(fill, target, backdrop, PALE_CONTRAST) : null,
865
+ ring: toneForContrast(fill, target, backdrop, RING_CONTRAST)
866
+ };
867
+ tones.set(fill, tone);
868
+ }
869
+ return tone;
870
+ };
871
+ const emphasis = context?.emphasis ?? null;
872
+ const labelField = fieldOf(map.encoding?.label);
873
+ const readLabel = labelField ? accessor(labelField) : null;
874
+ const labelFormat = map.encoding?.label?.format;
875
+ const bubbles = placed.map(({ o, x, y }) => {
876
+ const color = colors.fillOf(o.row);
877
+ const hollow = sized && o.size != null && o.size < 0;
878
+ const tone = toneOf(color.fill);
879
+ const rawLabel = readLabel ? readLabel(o.row) : void 0;
880
+ const labelKey = toKey(rawLabel);
881
+ return {
882
+ key: labelKey !== "" ? `l\0${labelKey}` : `p\0${o.lon},${o.lat}`,
883
+ x,
884
+ y,
885
+ r: radiusOf(o.size),
886
+ fill: color.fill,
887
+ ramp: color.ramp,
888
+ stroke: hollow ? tone.ring : tone.pale ?? bg,
889
+ outlined: !hollow && tone.pale != null,
890
+ hollow,
891
+ label: labelKey === "" ? "" : formatValue(rawLabel, labelFormat),
892
+ emph: rowAlpha(emphasis, o.row),
893
+ alpha: 1,
894
+ row: o.row,
895
+ index: o.index,
896
+ lon: o.lon,
897
+ lat: o.lat,
898
+ size: o.size,
899
+ colorValue: color.value,
900
+ colorNoData: color.noData,
901
+ rawLabel
902
+ };
903
+ });
904
+ bubbles.sort((a, b) => b.r - a.r || a.index - b.index);
905
+ const finalMarks = bubbles.map(toMark);
906
+ const morph = morphInput(context);
907
+ const moved = morph && Array.isArray(morph.from) ? morphKeyed(morph.from, finalMarks, morph.t, BUBBLE_MORPH) : null;
908
+ const entrance = !moved && context?.entranceProgress != null && context.entranceProgress < 1 ? Math.max(0, context.entranceProgress) : null;
909
+ const frame = moved ? [...moved.exits, ...moved.items] : entrance != null ? entranceMarks(finalMarks, entrance) : finalMarks;
910
+ stashMorphFrame(surface, "bubbleMap", frame);
911
+ const sketch = resolveSketch(map);
912
+ const pen = sketch ? new RoughPen(surface.marks.ctx, sketch) : null;
913
+ if (basemap) paintBasemap(surface, tokens, basemap, bg, pen);
914
+ else paintGraticule(surface, tokens, graticuleLines(kind, projection.project, fit, observations, projection.lon0), mapRect, pen);
915
+ paintBubbles(surface, frame, mapRect, pen, fillAlpha);
916
+ surface.evidence?.data(bubbles.length);
917
+ if (rampRect) drawRamp(surface, tokens, map, colors, rampRect);
918
+ const sizeKey = showLegend ? drawSizeLegend(surface, tokens, map, { mapRect, bubbles, sMax, limits, basemap, bg, pen }) : null;
919
+ if (map.labels === true && readLabel) {
920
+ const labelOpacity = entrance == null ? 1 : clamp((entrance - 0.8) / 0.2, 0, 1);
921
+ if (labelOpacity > 0) {
922
+ drawBubbleLabels(surface, tokens, {
923
+ marks: moved ? moved.items : finalMarks,
924
+ mapRect,
925
+ obstacles: sizeKey ? [sizeKey] : [],
926
+ opacity: labelOpacity,
927
+ bg,
928
+ backdrop,
929
+ fillAlpha
930
+ });
931
+ }
932
+ }
933
+ if (bubbles.length === 0) drawNoData(surface, tokens, content);
934
+ return buildInteraction(map, tokens, mapRect, bubbles, bg, planar);
935
+ }
936
+ function drawRamp(surface, tokens, map, colors, rect) {
937
+ if (!colors.domain) return;
938
+ drawColorRampLegend(surface, tokens, rect, {
939
+ domain: colors.domain,
940
+ scheme: colors.scheme,
941
+ format: map.encoding?.color?.format,
942
+ widthRatio: 0.5,
943
+ minWidth: 120,
944
+ maxWidth: 240,
945
+ path: "/encoding/color"
946
+ });
947
+ }
948
+ export {
949
+ drawBubbleMap
950
+ };
951
+ //# sourceMappingURL=bubbleMap.js.map