@opendata-ai/openchart-engine 8.0.0-rc.2 → 8.0.0-rc.20

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 +140 -2
  2. package/dist/index.js +1403 -210
  3. package/dist/index.js.map +1 -1
  4. package/package.json +3 -4
  5. package/src/__test-fixtures__/specs.ts +0 -182
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
  7. package/src/__tests__/annotation-behavior.test.ts +0 -721
  8. package/src/__tests__/axes.test.ts +0 -1579
  9. package/src/__tests__/compile-animation.test.ts +0 -161
  10. package/src/__tests__/compile-chart.test.ts +0 -1303
  11. package/src/__tests__/compile-footnotes.test.ts +0 -277
  12. package/src/__tests__/compile-layer.test.ts +0 -969
  13. package/src/__tests__/compile-snapshot.test.ts +0 -156
  14. package/src/__tests__/compound-labels.test.ts +0 -147
  15. package/src/__tests__/dimensions.test.ts +0 -918
  16. package/src/__tests__/encoding-sugar.test.ts +0 -390
  17. package/src/__tests__/facet.test.ts +0 -215
  18. package/src/__tests__/helpers/invariants.ts +0 -299
  19. package/src/__tests__/known-bugs.test.ts +0 -566
  20. package/src/__tests__/layout-fuzz.test.ts +0 -358
  21. package/src/__tests__/layout-invariants.test.ts +0 -535
  22. package/src/__tests__/legend.test.ts +0 -721
  23. package/src/__tests__/mark-keys.test.ts +0 -294
  24. package/src/__tests__/ranking-recipes.test.ts +0 -260
  25. package/src/__tests__/rotation.test.ts +0 -83
  26. package/src/__tests__/scales.test.ts +0 -323
  27. package/src/__tests__/series-search.test.ts +0 -92
  28. package/src/__tests__/spec-sugar.test.ts +0 -801
  29. package/src/__tests__/you-draw-it.test.ts +0 -160
  30. package/src/annotations/__tests__/compute.test.ts +0 -1672
  31. package/src/annotations/__tests__/geometry.test.ts +0 -66
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
  33. package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
  34. package/src/annotations/__tests__/placement.test.ts +0 -316
  35. package/src/annotations/__tests__/resolve-text.test.ts +0 -788
  36. package/src/annotations/__tests__/rich-text.test.ts +0 -91
  37. package/src/annotations/__tests__/thinning.test.ts +0 -285
  38. package/src/annotations/collisions.ts +0 -326
  39. package/src/annotations/compute.ts +0 -333
  40. package/src/annotations/constants.ts +0 -162
  41. package/src/annotations/geometry.ts +0 -370
  42. package/src/annotations/move.ts +0 -135
  43. package/src/annotations/placement.ts +0 -636
  44. package/src/annotations/position.ts +0 -131
  45. package/src/annotations/resolve-range.ts +0 -117
  46. package/src/annotations/resolve-refline.ts +0 -157
  47. package/src/annotations/resolve-text.ts +0 -539
  48. package/src/annotations/rich-text.ts +0 -65
  49. package/src/annotations/thinning.ts +0 -190
  50. package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
  51. package/src/barlist/compile-barlist.ts +0 -391
  52. package/src/barlist/types.ts +0 -28
  53. package/src/charts/__tests__/post-process.test.ts +0 -237
  54. package/src/charts/__tests__/registry.test.ts +0 -116
  55. package/src/charts/__tests__/utils.test.ts +0 -195
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
  57. package/src/charts/_shared/density-filter.ts +0 -26
  58. package/src/charts/_shared/format-label-value.ts +0 -16
  59. package/src/charts/bar/__tests__/compute.test.ts +0 -934
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
  61. package/src/charts/bar/__tests__/labels.test.ts +0 -292
  62. package/src/charts/bar/compute.ts +0 -539
  63. package/src/charts/bar/index.ts +0 -60
  64. package/src/charts/bar/labels.ts +0 -311
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
  67. package/src/charts/beeswarm/compute.ts +0 -203
  68. package/src/charts/beeswarm/dodge.ts +0 -155
  69. package/src/charts/beeswarm/index.ts +0 -32
  70. package/src/charts/builtin.ts +0 -75
  71. package/src/charts/calendar/__tests__/compute.test.ts +0 -417
  72. package/src/charts/calendar/compute.ts +0 -319
  73. package/src/charts/calendar/index.ts +0 -32
  74. package/src/charts/column/__tests__/compute.test.ts +0 -540
  75. package/src/charts/column/__tests__/labels.test.ts +0 -179
  76. package/src/charts/column/compute.ts +0 -493
  77. package/src/charts/column/index.ts +0 -53
  78. package/src/charts/column/labels.ts +0 -229
  79. package/src/charts/dot/__tests__/compute.test.ts +0 -380
  80. package/src/charts/dot/__tests__/labels.test.ts +0 -98
  81. package/src/charts/dot/compute.ts +0 -285
  82. package/src/charts/dot/index.ts +0 -62
  83. package/src/charts/dot/labels.ts +0 -121
  84. package/src/charts/line/__tests__/compute.test.ts +0 -1194
  85. package/src/charts/line/__tests__/curves.test.ts +0 -51
  86. package/src/charts/line/__tests__/labels.test.ts +0 -94
  87. package/src/charts/line/__tests__/mark-options.test.ts +0 -175
  88. package/src/charts/line/area.ts +0 -484
  89. package/src/charts/line/compute.ts +0 -356
  90. package/src/charts/line/curves.ts +0 -85
  91. package/src/charts/line/index.ts +0 -176
  92. package/src/charts/line/labels.ts +0 -177
  93. package/src/charts/parliament/__tests__/compute.test.ts +0 -411
  94. package/src/charts/parliament/compute.ts +0 -313
  95. package/src/charts/parliament/index.ts +0 -32
  96. package/src/charts/pie/__tests__/compute.test.ts +0 -384
  97. package/src/charts/pie/__tests__/labels.test.ts +0 -132
  98. package/src/charts/pie/compute.ts +0 -324
  99. package/src/charts/pie/index.ts +0 -49
  100. package/src/charts/pie/labels.ts +0 -143
  101. package/src/charts/post-process.ts +0 -229
  102. package/src/charts/range/__tests__/compute.test.ts +0 -486
  103. package/src/charts/range/compute.ts +0 -273
  104. package/src/charts/range/index.ts +0 -82
  105. package/src/charts/range/labels.ts +0 -169
  106. package/src/charts/rect/__tests__/compute.test.ts +0 -120
  107. package/src/charts/rect/index.ts +0 -127
  108. package/src/charts/registry.ts +0 -72
  109. package/src/charts/rule/index.ts +0 -139
  110. package/src/charts/scatter/__tests__/compute.test.ts +0 -488
  111. package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
  112. package/src/charts/scatter/compute.ts +0 -192
  113. package/src/charts/scatter/index.ts +0 -46
  114. package/src/charts/scatter/trendline.ts +0 -100
  115. package/src/charts/text/__tests__/compute.test.ts +0 -307
  116. package/src/charts/text/index.ts +0 -141
  117. package/src/charts/tick/__tests__/compute.test.ts +0 -75
  118. package/src/charts/tick/index.ts +0 -99
  119. package/src/charts/utils.ts +0 -170
  120. package/src/charts/waffle/__tests__/compute.test.ts +0 -305
  121. package/src/charts/waffle/compute.ts +0 -197
  122. package/src/charts/waffle/index.ts +0 -31
  123. package/src/compile/__tests__/color-scale-range.test.ts +0 -158
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
  125. package/src/compile/__tests__/data-clip.test.ts +0 -127
  126. package/src/compile/__tests__/fill-patterns.test.ts +0 -154
  127. package/src/compile/__tests__/size-scale.test.ts +0 -108
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
  129. package/src/compile/color-scale-range.ts +0 -101
  130. package/src/compile/data-clip.ts +0 -48
  131. package/src/compile/fill-patterns.ts +0 -74
  132. package/src/compile/layer.ts +0 -626
  133. package/src/compile/size-scale.ts +0 -139
  134. package/src/compile/spec-sugar.ts +0 -758
  135. package/src/compile/watermark-obstacle.ts +0 -54
  136. package/src/compile/you-draw-it.ts +0 -123
  137. package/src/compile.ts +0 -1450
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
  139. package/src/compiler/__tests__/animation.test.ts +0 -177
  140. package/src/compiler/__tests__/compile.test.ts +0 -87
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
  160. package/src/compiler/__tests__/keys.test.ts +0 -83
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
  162. package/src/compiler/__tests__/normalize.test.ts +0 -399
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -86
  165. package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +0 -89
  167. package/src/compiler/__tests__/validate.test.ts +0 -629
  168. package/src/compiler/__tests__/vl-parity.test.ts +0 -68
  169. package/src/compiler/a11y-warnings.ts +0 -110
  170. package/src/compiler/animation.ts +0 -142
  171. package/src/compiler/index.ts +0 -47
  172. package/src/compiler/keys.ts +0 -55
  173. package/src/compiler/normalize.ts +0 -733
  174. package/src/compiler/sparkline-defaults.ts +0 -138
  175. package/src/compiler/types.ts +0 -241
  176. package/src/compiler/validate.ts +0 -1810
  177. package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
  178. package/src/endpoint-labels/compute.ts +0 -621
  179. package/src/endpoint-labels/constants.ts +0 -54
  180. package/src/endpoint-labels/format.ts +0 -60
  181. package/src/endpoint-labels/predict.ts +0 -139
  182. package/src/format/__tests__/field-format.test.ts +0 -164
  183. package/src/format/field-format.ts +0 -27
  184. package/src/geo/__tests__/compile-map.test.ts +0 -621
  185. package/src/geo/compile-map.ts +0 -856
  186. package/src/geo/join.ts +0 -115
  187. package/src/geo/projections.ts +0 -45
  188. package/src/geo/types.ts +0 -24
  189. package/src/graphs/__tests__/community.test.ts +0 -228
  190. package/src/graphs/__tests__/compile-graph.test.ts +0 -396
  191. package/src/graphs/__tests__/encoding.test.ts +0 -415
  192. package/src/graphs/community.ts +0 -92
  193. package/src/graphs/compile-graph.ts +0 -329
  194. package/src/graphs/encoding.ts +0 -356
  195. package/src/graphs/types.ts +0 -106
  196. package/src/index.ts +0 -177
  197. package/src/layout/__tests__/metrics.test.ts +0 -97
  198. package/src/layout/__tests__/scales-highlight.test.ts +0 -192
  199. package/src/layout/axes/rotation.ts +0 -152
  200. package/src/layout/axes/thinning.ts +0 -98
  201. package/src/layout/axes/ticks.ts +0 -409
  202. package/src/layout/axes.ts +0 -757
  203. package/src/layout/dimensions.ts +0 -912
  204. package/src/layout/facet.ts +0 -91
  205. package/src/layout/gridlines.ts +0 -84
  206. package/src/layout/metrics.ts +0 -132
  207. package/src/layout/plan.ts +0 -631
  208. package/src/layout/scales.ts +0 -1053
  209. package/src/layout/shared.ts +0 -44
  210. package/src/legend/__tests__/continuous.test.ts +0 -318
  211. package/src/legend/__tests__/redundancy.test.ts +0 -108
  212. package/src/legend/__tests__/size.test.ts +0 -348
  213. package/src/legend/__tests__/suppression.test.ts +0 -382
  214. package/src/legend/compute.ts +0 -747
  215. package/src/legend/continuous.ts +0 -327
  216. package/src/legend/size.ts +0 -407
  217. package/src/legend/suppression.ts +0 -226
  218. package/src/legend/wrap.ts +0 -117
  219. package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
  220. package/src/sankey/__tests__/layout.test.ts +0 -165
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
  222. package/src/sankey/__tests__/node-sort.test.ts +0 -45
  223. package/src/sankey/compile-sankey.ts +0 -706
  224. package/src/sankey/layout.ts +0 -184
  225. package/src/sankey/types.ts +0 -41
  226. package/src/tables/__tests__/bar-column.test.ts +0 -147
  227. package/src/tables/__tests__/category-colors.test.ts +0 -230
  228. package/src/tables/__tests__/compile-table.test.ts +0 -236
  229. package/src/tables/__tests__/format-cells.test.ts +0 -143
  230. package/src/tables/__tests__/heatmap.test.ts +0 -148
  231. package/src/tables/__tests__/pagination.test.ts +0 -78
  232. package/src/tables/__tests__/search.test.ts +0 -94
  233. package/src/tables/__tests__/sort.test.ts +0 -107
  234. package/src/tables/__tests__/sparkline.test.ts +0 -122
  235. package/src/tables/bar-column.ts +0 -94
  236. package/src/tables/category-colors.ts +0 -78
  237. package/src/tables/compile-table.ts +0 -427
  238. package/src/tables/format-cells.ts +0 -115
  239. package/src/tables/heatmap.ts +0 -141
  240. package/src/tables/pagination.ts +0 -46
  241. package/src/tables/search.ts +0 -66
  242. package/src/tables/sort.ts +0 -69
  243. package/src/tables/sparkline.ts +0 -113
  244. package/src/tables/utils.ts +0 -16
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
  246. package/src/tilemap/compile-tilemap.ts +0 -755
  247. package/src/tilemap/layout.ts +0 -172
  248. package/src/tilemap/types.ts +0 -33
  249. package/src/tooltips/__tests__/compute.test.ts +0 -582
  250. package/src/tooltips/compute.ts +0 -700
  251. package/src/transforms/__tests__/aggregate.test.ts +0 -159
  252. package/src/transforms/__tests__/bin.test.ts +0 -88
  253. package/src/transforms/__tests__/calculate.test.ts +0 -146
  254. package/src/transforms/__tests__/conditional.test.ts +0 -127
  255. package/src/transforms/__tests__/filter-relative.test.ts +0 -202
  256. package/src/transforms/__tests__/filter.test.ts +0 -59
  257. package/src/transforms/__tests__/fold.test.ts +0 -79
  258. package/src/transforms/__tests__/index.test.ts +0 -93
  259. package/src/transforms/__tests__/predicates.test.ts +0 -193
  260. package/src/transforms/__tests__/timeunit.test.ts +0 -129
  261. package/src/transforms/__tests__/window.test.ts +0 -286
  262. package/src/transforms/aggregate.ts +0 -131
  263. package/src/transforms/bin.ts +0 -87
  264. package/src/transforms/calculate.ts +0 -60
  265. package/src/transforms/conditional.ts +0 -46
  266. package/src/transforms/filter.ts +0 -122
  267. package/src/transforms/fold.ts +0 -49
  268. package/src/transforms/index.ts +0 -60
  269. package/src/transforms/predicates.ts +0 -123
  270. package/src/transforms/timeunit.ts +0 -88
  271. package/src/transforms/window.ts +0 -185
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getHeightClass,
10
10
  getLayoutStrategy,
11
11
  isAxislessMark as isAxislessMark3,
12
+ MAX_SR_TABLE_ROWS,
12
13
  resolveTheme as resolveTheme7
13
14
  } from "@opendata-ai/openchart-core";
14
15
 
@@ -244,7 +245,10 @@ function refreshConnector(connector, bounds, markerRadius) {
244
245
  }
245
246
  const exit = connectorExit(bounds, endpoint.x, endpoint.y);
246
247
  if (!exit) return void 0;
247
- const from = { x: exit.x, y: exit.y };
248
+ const from = {
249
+ x: exit.x + (connector.fromOffset?.dx ?? 0),
250
+ y: exit.y + (connector.fromOffset?.dy ?? 0)
251
+ };
248
252
  const gap = connectorPullbackGap(markerRadius, connector.arrow);
249
253
  const cdx = endpoint.x - from.x;
250
254
  const cdy = endpoint.y - from.y;
@@ -506,6 +510,10 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
506
510
  from,
507
511
  to,
508
512
  endpoint,
513
+ fromOffset: annotation.connectorOffset?.from ? {
514
+ dx: annotation.connectorOffset.from.dx ?? 0,
515
+ dy: annotation.connectorOffset.from.dy ?? 0
516
+ } : void 0,
509
517
  stroke: connectorStroke,
510
518
  style: connectorStyle,
511
519
  arrow: connectorArrow,
@@ -5545,7 +5553,9 @@ var SIZE_SCALE_DEFAULTS = {
5545
5553
  * Text glyphs. Linear, not sqrt -- a word reads by its height, which is
5546
5554
  * linear in font size, where a disc reads by its area.
5547
5555
  */
5548
- text: { curve: "linear", range: [11, 32] }
5556
+ text: { curve: "linear", range: [11, 32] },
5557
+ /** Map point dots. sqrt for area-proportional, tighter range than scatter. */
5558
+ mapPoint: { curve: "sqrt", range: [3, 20] }
5549
5559
  };
5550
5560
  function sizeScaleDefaultsFor(markType) {
5551
5561
  const found = SIZE_SCALE_DEFAULTS[markType];
@@ -9078,7 +9088,7 @@ function resolvePosition2(value2, channelType, scale) {
9078
9088
  }
9079
9089
  }
9080
9090
  }
9081
- function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9091
+ function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
9082
9092
  const encoding = spec.encoding;
9083
9093
  const xChannel = encoding.x;
9084
9094
  const yChannel = encoding.y;
@@ -9096,6 +9106,10 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9096
9106
  const sizeField = sizeEnc?.field;
9097
9107
  const resolvedSize = buildSizeScale(sizeEnc, spec.data, SIZE_SCALE_DEFAULTS.point);
9098
9108
  const sizeScale = resolvedSize?.scale;
9109
+ const constFill = spec.markDef.fill;
9110
+ const constSize = spec.markDef.size;
9111
+ const bg = theme?.colors?.background;
9112
+ const defaultStroke = bg && bg !== "transparent" ? bg : "#ffffff";
9099
9113
  const keyEnc = encoding.key && "field" in encoding.key ? encoding.key : void 0;
9100
9114
  const keyField = keyEnc?.field;
9101
9115
  const marks = [];
@@ -9110,10 +9124,12 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9110
9124
  if (isSequentialColor && colorField) {
9111
9125
  const val = Number(row[colorField]);
9112
9126
  color2 = Number.isFinite(val) ? getSequentialColor(scales, val) : getColor(scales, "__default__");
9113
- } else {
9127
+ } else if (colorField) {
9114
9128
  color2 = getColor(scales, category ?? "__default__");
9129
+ } else {
9130
+ color2 = constFill ?? getColor(scales, "__default__");
9115
9131
  }
9116
- let radius = DEFAULT_POINT_RADIUS2;
9132
+ let radius = constSize != null ? constSize : DEFAULT_POINT_RADIUS2;
9117
9133
  if (sizeScale && sizeField) {
9118
9134
  const sizeVal = Number(row[sizeField]);
9119
9135
  if (Number.isFinite(sizeVal)) {
@@ -9134,9 +9150,9 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9134
9150
  cy,
9135
9151
  r: radius,
9136
9152
  fill: color2,
9137
- stroke: "#ffffff",
9138
- strokeWidth: 1,
9139
- fillOpacity: 0.7,
9153
+ stroke: spec.markDef.stroke ?? defaultStroke,
9154
+ strokeWidth: spec.markDef.strokeWidth ?? 1,
9155
+ fillOpacity: spec.markDef.opacity ?? 0.7,
9140
9156
  data: row,
9141
9157
  aria
9142
9158
  });
@@ -9177,7 +9193,7 @@ function linearRegression(points) {
9177
9193
  const intercept = (sumY - slope * sumX) / n;
9178
9194
  return { slope, intercept };
9179
9195
  }
9180
- function computeTrendLine(marks) {
9196
+ function computeTrendLine(marks, stroke = TRENDLINE_COLOR, strokeWidth = TRENDLINE_STROKE_WIDTH) {
9181
9197
  if (marks.length < 2) return null;
9182
9198
  const points = marks.map((m) => ({ x: m.cx, y: m.cy }));
9183
9199
  const result = linearRegression(points);
@@ -9200,8 +9216,8 @@ function computeTrendLine(marks) {
9200
9216
  { x: minX, y: y12 },
9201
9217
  { x: maxX, y: y2 }
9202
9218
  ],
9203
- stroke: TRENDLINE_COLOR,
9204
- strokeWidth: TRENDLINE_STROKE_WIDTH,
9219
+ stroke,
9220
+ strokeWidth,
9205
9221
  strokeDasharray: TRENDLINE_DASH,
9206
9222
  data: [],
9207
9223
  aria
@@ -9209,13 +9225,20 @@ function computeTrendLine(marks) {
9209
9225
  }
9210
9226
 
9211
9227
  // src/charts/scatter/index.ts
9212
- var scatterRenderer = (spec, scales, chartArea, strategy, _theme) => {
9213
- const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy);
9228
+ var scatterRenderer = (spec, scales, chartArea, strategy, theme) => {
9229
+ const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy, theme);
9214
9230
  const marks = [...pointMarks];
9215
- if (spec.markDef?.trendline !== false) {
9216
- const trendLine = computeTrendLine(pointMarks);
9231
+ const trendline = spec.markDef?.trendline;
9232
+ if (trendline !== false) {
9233
+ const config = typeof trendline === "object" ? trendline : void 0;
9234
+ const stroke = config?.stroke ?? theme?.colors?.text;
9235
+ const trendLine = computeTrendLine(pointMarks, stroke, config?.strokeWidth);
9217
9236
  if (trendLine) {
9218
- marks.unshift(trendLine);
9237
+ if (config?.layer === "above") {
9238
+ marks.push(trendLine);
9239
+ } else {
9240
+ marks.unshift(trendLine);
9241
+ }
9219
9242
  }
9220
9243
  }
9221
9244
  return marks;
@@ -9837,7 +9860,16 @@ function expandChannelSugar(spec, warnings) {
9837
9860
  }
9838
9861
  return result;
9839
9862
  }
9840
- var COUNT_GROUP_CHANNELS = ["x", "y", "color", "detail", "strokeDash", "facet"];
9863
+ var COUNT_GROUP_CHANNELS = [
9864
+ "x",
9865
+ "y",
9866
+ "color",
9867
+ "detail",
9868
+ "strokeDash",
9869
+ "facet",
9870
+ "row",
9871
+ "column"
9872
+ ];
9841
9873
  function expandCountAggregate(spec) {
9842
9874
  const encoding = spec.encoding;
9843
9875
  if (!encoding || typeof encoding !== "object") return spec;
@@ -10072,6 +10104,12 @@ function resolveAnimation(spec) {
10072
10104
  const enter = resolvePhase(config.enter, ENTER_DEFAULTS);
10073
10105
  const update = resolvePhase(config.update, UPDATE_DEFAULTS);
10074
10106
  const exit = resolvePhase(config.exit, EXIT_DEFAULTS);
10107
+ if (update && config.update && typeof config.update === "object" && !Array.isArray(config.update)) {
10108
+ const cap = config.update.maxMarks;
10109
+ if (typeof cap === "number" && Number.isFinite(cap) && cap >= 1) {
10110
+ update.maxMarks = Math.floor(cap);
10111
+ }
10112
+ }
10075
10113
  if (!enter && !update && !exit) return void 0;
10076
10114
  return {
10077
10115
  enter: enter ?? void 0,
@@ -10464,6 +10502,7 @@ function normalizeChartSpec(spec, warnings) {
10464
10502
  data: spec.data,
10465
10503
  encoding,
10466
10504
  chrome: normalizeChrome(spec.chrome),
10505
+ chromeLayout: spec.chromeLayout,
10467
10506
  metrics: spec.metrics,
10468
10507
  seriesSearch: normalizeSeriesSearch(spec, encoding, warnings),
10469
10508
  youDrawIt: normalizeYouDrawIt(spec),
@@ -10538,15 +10577,10 @@ function normalizeSankeySpec(spec, _warnings) {
10538
10577
  };
10539
10578
  }
10540
10579
  function normalizeGraphSpec(spec) {
10541
- const defaultLayout = {
10580
+ const layout = {
10542
10581
  type: "force",
10543
- chargeStrength: -300,
10544
- linkDistance: 30
10545
- };
10546
- const layout = spec.layout ? {
10547
- ...defaultLayout,
10548
10582
  ...spec.layout
10549
- } : defaultLayout;
10583
+ };
10550
10584
  return {
10551
10585
  type: "graph",
10552
10586
  nodes: spec.nodes,
@@ -10558,7 +10592,10 @@ function normalizeGraphSpec(spec) {
10558
10592
  annotations: normalizeAnnotations(spec.annotations),
10559
10593
  theme: spec.theme ?? {},
10560
10594
  darkMode: spec.darkMode ?? "off",
10561
- watermark: spec.watermark ?? true
10595
+ watermark: spec.watermark ?? true,
10596
+ animation: spec.animation,
10597
+ interaction: spec.interaction,
10598
+ legend: spec.legend
10562
10599
  };
10563
10600
  }
10564
10601
  function normalizeTileMapSpec(spec, warnings) {
@@ -10631,7 +10668,8 @@ function normalizeMapSpec(spec, _warnings) {
10631
10668
  darkMode: spec.darkMode ?? "off",
10632
10669
  watermark: spec.watermark ?? true,
10633
10670
  animation: spec.animation,
10634
- valueFormat: spec.valueFormat
10671
+ valueFormat: spec.valueFormat,
10672
+ points: spec.points
10635
10673
  };
10636
10674
  }
10637
10675
  function normalizeBarListSpec(spec, _warnings) {
@@ -10726,6 +10764,7 @@ import {
10726
10764
  MARK_ENCODING_RULES,
10727
10765
  MARK_TYPES,
10728
10766
  resolveSchemeName as resolveSchemeName2,
10767
+ SEQUENTIAL_PALETTES,
10729
10768
  SUPPORTED_SCHEME_NAMES
10730
10769
  } from "@opendata-ai/openchart-core";
10731
10770
  var VALID_FIELD_TYPES = /* @__PURE__ */ new Set(["quantitative", "temporal", "nominal", "ordinal"]);
@@ -10784,6 +10823,53 @@ function didYouMean(field, columns) {
10784
10823
  const match = nearestColumn(field, columns);
10785
10824
  return match ? ` Did you mean "${match}"?` : "";
10786
10825
  }
10826
+ function checkSchemeName(channelObj, channelPath, errors) {
10827
+ const scale = channelObj.scale;
10828
+ if (scale && typeof scale.scheme === "string" && !resolveSchemeName2(scale.scheme)) {
10829
+ errors.push({
10830
+ message: `Spec error: ${channelPath}.scale.scheme "${scale.scheme}" is not a supported scheme name`,
10831
+ path: `${channelPath}.scale.scheme`,
10832
+ code: "INVALID_VALUE",
10833
+ suggestion: `Use one of the supported scheme names: ${[...SUPPORTED_SCHEME_NAMES].join(", ")}. Or provide explicit colors via scale.range.`
10834
+ });
10835
+ }
10836
+ }
10837
+ function checkSchemeUnused(encoding, errors, familyNote, suggestion) {
10838
+ for (const [channel, ch] of Object.entries(encoding)) {
10839
+ if (!ch || typeof ch !== "object" || Array.isArray(ch)) continue;
10840
+ const scale = ch.scale;
10841
+ if (scale && typeof scale.scheme === "string") {
10842
+ errors.push({
10843
+ message: `Spec error: encoding.${channel}.scale.scheme has no effect \u2014 ${familyNote}`,
10844
+ path: `encoding.${channel}.scale.scheme`,
10845
+ code: "INVALID_VALUE",
10846
+ suggestion
10847
+ });
10848
+ }
10849
+ }
10850
+ }
10851
+ var MAP_PALETTE_NAMES = Object.keys(SEQUENTIAL_PALETTES).join(", ");
10852
+ function checkMapSchemeName(channelObj, channelPath, errors) {
10853
+ const scale = channelObj.scale;
10854
+ if (!scale || typeof scale.scheme !== "string") return;
10855
+ if (channelObj.type === "nominal" || channelObj.type === "ordinal") {
10856
+ errors.push({
10857
+ message: `Spec error: ${channelPath}.scale.scheme has no effect \u2014 categorical map colors come from scale.range and scale.domain`,
10858
+ path: `${channelPath}.scale.scheme`,
10859
+ code: "INVALID_VALUE",
10860
+ suggestion: "Remove scale.scheme. To customize categorical map colors, provide explicit colors via scale.range (with scale.domain for category order)."
10861
+ });
10862
+ return;
10863
+ }
10864
+ if (!(scale.scheme in SEQUENTIAL_PALETTES)) {
10865
+ errors.push({
10866
+ message: `Spec error: ${channelPath}.scale.scheme "${scale.scheme}" is not a supported map palette`,
10867
+ path: `${channelPath}.scale.scheme`,
10868
+ code: "INVALID_VALUE",
10869
+ suggestion: `Maps support the sequential palettes: ${MAP_PALETTE_NAMES}. The default is "blue".`
10870
+ });
10871
+ }
10872
+ }
10787
10873
  var VALID_RANGE_STYLES = /* @__PURE__ */ new Set(["dumbbell", "arrow", "bar"]);
10788
10874
  function validateRangeSpec(spec, errors) {
10789
10875
  const encoding = spec.encoding;
@@ -10942,6 +11028,21 @@ function validateCalendarSpec(spec, errors) {
10942
11028
  seenDays.set(dayKey, i);
10943
11029
  }
10944
11030
  }
11031
+ var VALID_RENDER_MODES = /* @__PURE__ */ new Set(["auto", "svg", "canvas"]);
11032
+ function validateMarkRenderMode(spec, errors) {
11033
+ const mark = spec.mark;
11034
+ if (!mark || typeof mark !== "object" || Array.isArray(mark)) return;
11035
+ const render = mark.render;
11036
+ if (render === void 0) return;
11037
+ if (typeof render !== "string" || !VALID_RENDER_MODES.has(render)) {
11038
+ errors.push({
11039
+ message: `Spec error: mark.render ${JSON.stringify(render)} is not valid. Must be one of: auto, svg, canvas`,
11040
+ path: "mark.render",
11041
+ code: "INVALID_VALUE",
11042
+ suggestion: 'Use mark: { type: "point", render: "auto" } (default), "canvas" to force the canvas mark layer, or "svg" to force one circle per point.'
11043
+ });
11044
+ }
11045
+ }
10945
11046
  function validateChartSpec(spec, errors) {
10946
11047
  const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
10947
11048
  if (!Array.isArray(spec.data)) {
@@ -11047,6 +11148,7 @@ function validateChartSpec(spec, errors) {
11047
11148
  if (markType === "calendar") {
11048
11149
  validateCalendarSpec(spec, errors);
11049
11150
  }
11151
+ validateMarkRenderMode(spec, errors);
11050
11152
  if (Array.isArray(spec.transform)) {
11051
11153
  const transforms = spec.transform;
11052
11154
  for (let i = 0; i < transforms.length; i++) {
@@ -11145,15 +11247,7 @@ function validateChartSpec(spec, errors) {
11145
11247
  suggestion: `Use one of: ${[...VALID_FIELD_TYPES].join(", ")}`
11146
11248
  });
11147
11249
  }
11148
- const channelScale = channelObj.scale;
11149
- if (channelScale && typeof channelScale.scheme === "string" && !resolveSchemeName2(channelScale.scheme)) {
11150
- errors.push({
11151
- message: `Spec error: encoding.${channel}.scale.scheme "${channelScale.scheme}" is not a supported scheme name`,
11152
- path: `encoding.${channel}.scale.scheme`,
11153
- code: "INVALID_VALUE",
11154
- suggestion: `Use one of the supported scheme names: ${[...SUPPORTED_SCHEME_NAMES].join(", ")}. Or provide explicit colors via scale.range.`
11155
- });
11156
- }
11250
+ checkSchemeName(channelObj, `encoding.${channel}`, errors);
11157
11251
  if (channelRule && channelObj.type && channelRule.allowedTypes.length > 0) {
11158
11252
  if (!channelRule.allowedTypes.includes(channelObj.type)) {
11159
11253
  errors.push({
@@ -11267,6 +11361,52 @@ function validateChartSpec(spec, errors) {
11267
11361
  }
11268
11362
  }
11269
11363
  }
11364
+ for (const channel of ["row", "column"]) {
11365
+ const ch = encoding[channel];
11366
+ if (ch && typeof ch === "object") {
11367
+ const facetCh = ch;
11368
+ if (!facetCh.field || typeof facetCh.field !== "string") {
11369
+ errors.push({
11370
+ message: `Spec error: encoding.${channel}.field is required and must be a non-empty string`,
11371
+ path: `encoding.${channel}.field`,
11372
+ code: "MISSING_FIELD",
11373
+ suggestion: `Add a field property to encoding.${channel} (e.g. { field: "category", type: "nominal" })`
11374
+ });
11375
+ } else if (!dataColumns.has(facetCh.field) && !transformFields.has(facetCh.field)) {
11376
+ errors.push({
11377
+ message: `Spec error: encoding.${channel}.field "${facetCh.field}" does not exist in data. Available columns: ${availableColumns}`,
11378
+ path: `encoding.${channel}.field`,
11379
+ code: "DATA_FIELD_MISSING",
11380
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(facetCh.field, columnList)}`
11381
+ });
11382
+ }
11383
+ if (!facetCh.type || facetCh.type !== "nominal" && facetCh.type !== "ordinal") {
11384
+ errors.push({
11385
+ message: `Spec error: encoding.${channel}.type must be "nominal" or "ordinal"${facetCh.type ? `, got "${facetCh.type}"` : ""}`,
11386
+ path: `encoding.${channel}.type`,
11387
+ code: facetCh.type ? "INVALID_VALUE" : "MISSING_FIELD",
11388
+ suggestion: `Set encoding.${channel}.type to "nominal" or "ordinal"`
11389
+ });
11390
+ }
11391
+ if (encoding.facet && typeof encoding.facet === "object") {
11392
+ errors.push({
11393
+ message: `Spec error: encoding.${channel} and encoding.facet cannot be used together`,
11394
+ path: `encoding.${channel}`,
11395
+ code: "INVALID_VALUE",
11396
+ suggestion: `Use either encoding.${channel} or encoding.facet, not both`
11397
+ });
11398
+ }
11399
+ const otherChannel = channel === "row" ? "column" : "row";
11400
+ if (encoding[otherChannel]) {
11401
+ errors.push({
11402
+ message: "Spec error: encoding.row and encoding.column cannot be used together (cross-product faceting is not yet supported)",
11403
+ path: `encoding.${channel}`,
11404
+ code: "INVALID_VALUE",
11405
+ suggestion: "Use encoding.facet with columns for grid layout, or use only row or column"
11406
+ });
11407
+ }
11408
+ }
11409
+ }
11270
11410
  if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
11271
11411
  errors.push({
11272
11412
  message: `Spec error: darkMode must be "auto", "force", or "off"`,
@@ -11479,7 +11619,7 @@ function validateGraphSpec(spec, errors) {
11479
11619
  message: `Spec error: edges[${i}].source "${edge.source}" does not reference an existing node id`,
11480
11620
  path: `edges[${i}].source`,
11481
11621
  code: "DATA_FIELD_MISSING",
11482
- suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}`
11622
+ suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}.${didYouMean(edge.source, [...nodeIds])}`
11483
11623
  });
11484
11624
  }
11485
11625
  if (typeof edge.target !== "string" || edge.target === "") {
@@ -11494,40 +11634,60 @@ function validateGraphSpec(spec, errors) {
11494
11634
  message: `Spec error: edges[${i}].target "${edge.target}" does not reference an existing node id`,
11495
11635
  path: `edges[${i}].target`,
11496
11636
  code: "DATA_FIELD_MISSING",
11497
- suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}`
11637
+ suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}.${didYouMean(edge.target, [...nodeIds])}`
11498
11638
  });
11499
11639
  }
11500
11640
  }
11501
11641
  if (spec.encoding && typeof spec.encoding === "object") {
11502
11642
  const encoding = spec.encoding;
11503
- const firstNode = nodes[0];
11504
- const firstEdge = edges.length > 0 ? edges[0] : null;
11505
- const nodeFields = firstNode ? new Set(Object.keys(firstNode)) : /* @__PURE__ */ new Set();
11506
- const edgeFields = firstEdge ? new Set(Object.keys(firstEdge)) : /* @__PURE__ */ new Set();
11507
- const nodeChannels = ["nodeColor", "nodeSize", "nodeLabel"];
11643
+ const nodeFields = /* @__PURE__ */ new Set();
11644
+ for (const node of nodes) {
11645
+ if (node && typeof node === "object") {
11646
+ for (const key of Object.keys(node)) nodeFields.add(key);
11647
+ }
11648
+ }
11649
+ const edgeFields = /* @__PURE__ */ new Set();
11650
+ for (const edge of edges) {
11651
+ if (edge && typeof edge === "object") {
11652
+ for (const key of Object.keys(edge)) edgeFields.add(key);
11653
+ }
11654
+ }
11655
+ const nodeChannels = [
11656
+ "nodeColor",
11657
+ "nodeSize",
11658
+ "nodeOpacity",
11659
+ "nodeLabel",
11660
+ "nodeLabelPriority"
11661
+ ];
11508
11662
  for (const channel of nodeChannels) {
11509
11663
  const ch = encoding[channel];
11510
- if (ch?.field && typeof ch.field === "string" && !nodeFields.has(ch.field)) {
11664
+ if (ch?.field && typeof ch.field === "string" && nodes.length > 0 && !nodeFields.has(ch.field)) {
11511
11665
  errors.push({
11512
11666
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
11513
11667
  path: `encoding.${channel}.field`,
11514
11668
  code: "DATA_FIELD_MISSING",
11515
- suggestion: `Use one of the node fields: ${[...nodeFields].join(", ")}`
11669
+ suggestion: `Use one of the node fields: ${[...nodeFields].join(", ")}.${didYouMean(ch.field, [...nodeFields])}`
11516
11670
  });
11517
11671
  }
11518
11672
  }
11519
- const edgeChannels = ["edgeColor", "edgeWidth"];
11673
+ const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
11520
11674
  for (const channel of edgeChannels) {
11521
11675
  const ch = encoding[channel];
11522
- if (ch?.field && typeof ch.field === "string" && firstEdge && !edgeFields.has(ch.field)) {
11676
+ if (ch?.field && typeof ch.field === "string" && edges.length > 0 && !edgeFields.has(ch.field)) {
11523
11677
  errors.push({
11524
11678
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
11525
11679
  path: `encoding.${channel}.field`,
11526
11680
  code: "DATA_FIELD_MISSING",
11527
- suggestion: `Use one of the edge fields: ${[...edgeFields].join(", ")}`
11681
+ suggestion: `Use one of the edge fields: ${[...edgeFields].join(", ")}.${didYouMean(ch.field, [...edgeFields])}`
11528
11682
  });
11529
11683
  }
11530
11684
  }
11685
+ checkSchemeUnused(
11686
+ encoding,
11687
+ errors,
11688
+ "graph colors come from scale.range and scale.domain",
11689
+ "Remove scale.scheme. To customize graph colors, provide explicit colors via scale.range."
11690
+ );
11531
11691
  }
11532
11692
  if (spec.layout && typeof spec.layout === "object") {
11533
11693
  const layout = spec.layout;
@@ -11581,7 +11741,8 @@ function validateSankeySpec(spec, errors) {
11581
11741
  }
11582
11742
  const encoding = spec.encoding;
11583
11743
  const dataColumns = new Set(Object.keys(firstRow));
11584
- const availableColumns = [...dataColumns].join(", ");
11744
+ const columnList = [...dataColumns];
11745
+ const availableColumns = columnList.join(", ");
11585
11746
  for (const channel of ["source", "target", "value"]) {
11586
11747
  const ch = encoding[channel];
11587
11748
  if (!ch || typeof ch !== "object") {
@@ -11589,7 +11750,7 @@ function validateSankeySpec(spec, errors) {
11589
11750
  message: `Spec error: sankey encoding requires "${channel}" channel`,
11590
11751
  path: `encoding.${channel}`,
11591
11752
  code: "MISSING_FIELD",
11592
- suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
11753
+ suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
11593
11754
  });
11594
11755
  continue;
11595
11756
  }
@@ -11607,10 +11768,16 @@ function validateSankeySpec(spec, errors) {
11607
11768
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
11608
11769
  path: `encoding.${channel}.field`,
11609
11770
  code: "DATA_FIELD_MISSING",
11610
- suggestion: `Use one of the available data columns: ${availableColumns}`
11771
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
11611
11772
  });
11612
11773
  }
11613
11774
  }
11775
+ checkSchemeUnused(
11776
+ encoding,
11777
+ errors,
11778
+ "sankey node colors cycle the theme categorical palette",
11779
+ "Remove scale.scheme. To customize sankey colors, set theme.colors.categorical."
11780
+ );
11614
11781
  if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
11615
11782
  errors.push({
11616
11783
  message: 'Spec error: darkMode must be "auto", "force", or "off"',
@@ -11670,7 +11837,8 @@ function validateTileMapSpec(spec, errors) {
11670
11837
  }
11671
11838
  const encoding = spec.encoding;
11672
11839
  const dataColumns = new Set(Object.keys(firstRow));
11673
- const availableColumns = [...dataColumns].join(", ");
11840
+ const columnList = [...dataColumns];
11841
+ const availableColumns = columnList.join(", ");
11674
11842
  for (const channel of ["state", "value"]) {
11675
11843
  const ch = encoding[channel];
11676
11844
  if (!ch || typeof ch !== "object") {
@@ -11678,7 +11846,7 @@ function validateTileMapSpec(spec, errors) {
11678
11846
  message: `Spec error: tilemap encoding requires "${channel}" channel`,
11679
11847
  path: `encoding.${channel}`,
11680
11848
  code: "MISSING_FIELD",
11681
- suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
11849
+ suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
11682
11850
  });
11683
11851
  continue;
11684
11852
  }
@@ -11696,11 +11864,19 @@ function validateTileMapSpec(spec, errors) {
11696
11864
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
11697
11865
  path: `encoding.${channel}.field`,
11698
11866
  code: "DATA_FIELD_MISSING",
11699
- suggestion: `Use one of the available data columns: ${availableColumns}`
11867
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
11700
11868
  });
11701
11869
  }
11702
11870
  }
11703
11871
  }
11872
+ if (spec.encoding && typeof spec.encoding === "object") {
11873
+ checkSchemeUnused(
11874
+ spec.encoding,
11875
+ errors,
11876
+ "tilemap fills come from the top-level palette property",
11877
+ `Remove scale.scheme and set the top-level palette property instead, e.g. palette: "green". Supported: ${MAP_PALETTE_NAMES}.`
11878
+ );
11879
+ }
11704
11880
  if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
11705
11881
  errors.push({
11706
11882
  message: 'Spec error: darkMode must be "auto", "force", or "off"',
@@ -11739,6 +11915,16 @@ function validateMapSpec(spec, errors) {
11739
11915
  });
11740
11916
  return;
11741
11917
  }
11918
+ const mapEncoding = spec.encoding;
11919
+ if (mapEncoding.color && typeof mapEncoding.color === "object") {
11920
+ checkMapSchemeName(mapEncoding.color, "encoding.color", errors);
11921
+ }
11922
+ if (spec.points && typeof spec.points === "object") {
11923
+ const pointsLayer = spec.points;
11924
+ if (pointsLayer.color && typeof pointsLayer.color === "object") {
11925
+ checkMapSchemeName(pointsLayer.color, "points.color", errors);
11926
+ }
11927
+ }
11742
11928
  if ("focus" in geo && geo.focus !== void 0 && geo.focus !== null) {
11743
11929
  const focus = geo.focus;
11744
11930
  if (typeof focus === "string" || typeof focus === "number") {
@@ -11755,12 +11941,31 @@ function validateMapSpec(spec, errors) {
11755
11941
  }
11756
11942
  } else if (typeof focus === "object") {
11757
11943
  const focusObj = focus;
11758
- if (!("features" in focusObj) || focusObj.features == null) {
11944
+ if ("points" in focusObj) {
11945
+ const pts = focusObj.points;
11946
+ const isSubset = typeof pts === "object" && pts !== null && typeof pts.field === "string" && "value" in pts;
11947
+ if (pts !== true && !isSubset) {
11948
+ errors.push({
11949
+ message: "Spec error: geo.focus.points must be true or { field: string, value: string | number }",
11950
+ path: "geo.focus.points",
11951
+ code: "INVALID_TYPE",
11952
+ suggestion: 'Use { points: true } to fit all points, or { points: { field: "rating", value: "F" } } to fit a subset.'
11953
+ });
11954
+ }
11955
+ if (focusObj.padding !== void 0 && typeof focusObj.padding !== "number") {
11956
+ errors.push({
11957
+ message: "Spec error: geo.focus.padding must be a number",
11958
+ path: "geo.focus.padding",
11959
+ code: "INVALID_TYPE",
11960
+ suggestion: "Provide padding as a number in map-local units, e.g. padding: 16"
11961
+ });
11962
+ }
11963
+ } else if (!("features" in focusObj) || focusObj.features == null) {
11759
11964
  errors.push({
11760
- message: 'Spec error: geo.focus object must have a "features" property',
11965
+ message: 'Spec error: geo.focus object must have a "features" or "points" property',
11761
11966
  path: "geo.focus.features",
11762
11967
  code: "MISSING_FIELD",
11763
- suggestion: 'Provide feature id(s) to focus on, e.g. geo: { focus: { features: "36", padding: 32 } }'
11968
+ suggestion: 'Provide feature id(s) to focus on, e.g. { features: "36", padding: 32 }, or { points: true } to fit the point cluster.'
11764
11969
  });
11765
11970
  } else {
11766
11971
  const feats = focusObj.features;
@@ -11794,32 +11999,98 @@ function validateMapSpec(spec, errors) {
11794
11999
  }
11795
12000
  } else {
11796
12001
  errors.push({
11797
- message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? }",
12002
+ message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? } or { points: true, padding? }",
11798
12003
  path: "geo.focus",
11799
12004
  code: "INVALID_TYPE",
11800
- suggestion: 'Use a feature id, array of ids, or { features: "36", padding: 16 }. Pass null to clear focus.'
12005
+ suggestion: 'Use a feature id, array of ids, { features: "36", padding: 16 }, or { points: true }. Pass null to clear focus.'
11801
12006
  });
11802
12007
  }
11803
12008
  }
11804
12009
  const encoding = spec.encoding;
11805
- for (const channel of ["key", "color"]) {
11806
- const ch = encoding[channel];
11807
- if (!ch || typeof ch !== "object") {
12010
+ const hasPoints = spec.points && typeof spec.points === "object" && !Array.isArray(spec.points);
12011
+ const specData = spec.data;
12012
+ const isBasemapOnly = hasPoints && (!specData || specData.length === 0);
12013
+ const keyCh = encoding.key;
12014
+ if (!isBasemapOnly) {
12015
+ if (!keyCh || typeof keyCh !== "object") {
11808
12016
  errors.push({
11809
- message: `Spec error: map encoding requires "${channel}" channel`,
11810
- path: `encoding.${channel}`,
12017
+ message: 'Spec error: map encoding requires "key" channel',
12018
+ path: "encoding.key",
11811
12019
  code: "MISSING_FIELD",
11812
- suggestion: `Add encoding.${channel}, e.g. ${channel}: { field: "${channel === "key" ? "id" : "value"}", type: "${channel === "key" ? "nominal" : "quantitative"}" }`
12020
+ suggestion: 'Add encoding.key, e.g. key: { field: "id", type: "nominal" }'
12021
+ });
12022
+ } else if (!keyCh.field || typeof keyCh.field !== "string") {
12023
+ errors.push({
12024
+ message: 'Spec error: encoding.key must have a "field" string',
12025
+ path: "encoding.key.field",
12026
+ code: "MISSING_FIELD",
12027
+ suggestion: 'Add a field name, e.g. key: { field: "id" }'
11813
12028
  });
11814
- continue;
11815
12029
  }
11816
- if (!ch.field || typeof ch.field !== "string") {
12030
+ }
12031
+ const colorCh = encoding.color;
12032
+ if (!hasPoints) {
12033
+ if (!colorCh || typeof colorCh !== "object") {
11817
12034
  errors.push({
11818
- message: `Spec error: encoding.${channel} must have a "field" string`,
11819
- path: `encoding.${channel}.field`,
12035
+ message: 'Spec error: map encoding requires "color" channel (or add a "points" layer for basemap-only mode)',
12036
+ path: "encoding.color",
11820
12037
  code: "MISSING_FIELD",
11821
- suggestion: `Add a field name, e.g. ${channel}: { field: "${channel === "key" ? "id" : "value"}" }`
12038
+ suggestion: 'Add encoding.color, e.g. color: { field: "value", type: "quantitative" }'
11822
12039
  });
12040
+ } else if (!colorCh.field || typeof colorCh.field !== "string") {
12041
+ errors.push({
12042
+ message: 'Spec error: encoding.color must have a "field" string',
12043
+ path: "encoding.color.field",
12044
+ code: "MISSING_FIELD",
12045
+ suggestion: 'Add a field name, e.g. color: { field: "value" }'
12046
+ });
12047
+ }
12048
+ } else if (colorCh && typeof colorCh === "object" && (!colorCh.field || typeof colorCh.field !== "string")) {
12049
+ errors.push({
12050
+ message: 'Spec error: encoding.color must have a "field" string when provided',
12051
+ path: "encoding.color.field",
12052
+ code: "MISSING_FIELD",
12053
+ suggestion: 'Add a field name, e.g. color: { field: "value" }'
12054
+ });
12055
+ }
12056
+ if (hasPoints) {
12057
+ const points = spec.points;
12058
+ if (!Array.isArray(points.data)) {
12059
+ errors.push({
12060
+ message: "Spec error: points.data must be an array of data rows",
12061
+ path: "points.data",
12062
+ code: "INVALID_TYPE",
12063
+ suggestion: "Provide data as an array of objects with lat/lng fields"
12064
+ });
12065
+ }
12066
+ for (const ch of ["longitude", "latitude"]) {
12067
+ const enc = points[ch];
12068
+ if (!enc || typeof enc !== "object") {
12069
+ errors.push({
12070
+ message: `Spec error: points requires "${ch}" encoding channel`,
12071
+ path: `points.${ch}`,
12072
+ code: "MISSING_FIELD",
12073
+ suggestion: `Add points.${ch}, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}", type: "quantitative" }`
12074
+ });
12075
+ } else if (!enc.field || typeof enc.field !== "string") {
12076
+ errors.push({
12077
+ message: `Spec error: points.${ch} must have a "field" string`,
12078
+ path: `points.${ch}.field`,
12079
+ code: "MISSING_FIELD",
12080
+ suggestion: `Add a field name, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}" }`
12081
+ });
12082
+ }
12083
+ }
12084
+ for (const ch of ["size", "color", "key"]) {
12085
+ const enc = points[ch];
12086
+ if (enc && typeof enc === "object" && (!enc.field || typeof enc.field !== "string")) {
12087
+ errors.push({
12088
+ message: `Spec error: points.${ch} must have a "field" string when provided`,
12089
+ path: `points.${ch}.field`,
12090
+ code: "MISSING_FIELD",
12091
+ suggestion: `Add a field name, e.g. ${ch}: { field: "myField" }`
12092
+ });
12093
+ }
11823
12094
  }
11824
12095
  }
11825
12096
  }
@@ -11863,7 +12134,8 @@ function validateBarListSpec(spec, errors) {
11863
12134
  }
11864
12135
  const encoding = spec.encoding;
11865
12136
  const dataColumns = new Set(Object.keys(firstRow));
11866
- const availableColumns = [...dataColumns].join(", ");
12137
+ const columnList = [...dataColumns];
12138
+ const availableColumns = columnList.join(", ");
11867
12139
  for (const channel of ["label", "value"]) {
11868
12140
  const ch = encoding[channel];
11869
12141
  if (!ch || typeof ch !== "object") {
@@ -11871,7 +12143,7 @@ function validateBarListSpec(spec, errors) {
11871
12143
  message: `Spec error: barlist encoding requires "${channel}" channel`,
11872
12144
  path: `encoding.${channel}`,
11873
12145
  code: "MISSING_FIELD",
11874
- suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
12146
+ suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
11875
12147
  });
11876
12148
  continue;
11877
12149
  }
@@ -11889,7 +12161,7 @@ function validateBarListSpec(spec, errors) {
11889
12161
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
11890
12162
  path: `encoding.${channel}.field`,
11891
12163
  code: "DATA_FIELD_MISSING",
11892
- suggestion: `Use one of the available data columns: ${availableColumns}`
12164
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
11893
12165
  });
11894
12166
  }
11895
12167
  }
@@ -11902,10 +12174,16 @@ function validateBarListSpec(spec, errors) {
11902
12174
  message: `Spec error: encoding.${channel}.field "${field}" does not exist in data. Available columns: ${availableColumns}`,
11903
12175
  path: `encoding.${channel}.field`,
11904
12176
  code: "DATA_FIELD_MISSING",
11905
- suggestion: `Use one of the available data columns: ${availableColumns}`
12177
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(field, columnList)}`
11906
12178
  });
11907
12179
  }
11908
12180
  }
12181
+ checkSchemeUnused(
12182
+ encoding,
12183
+ errors,
12184
+ "bar list colors cycle the built-in barlist palette",
12185
+ "Remove scale.scheme. Bar list colors are not configurable via encoding; they cycle the built-in palette."
12186
+ );
11909
12187
  if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
11910
12188
  errors.push({
11911
12189
  message: 'Spec error: darkMode must be "auto", "force", or "off"',
@@ -11988,6 +12266,22 @@ function validateLayerSpec(spec, errors) {
11988
12266
  }
11989
12267
  }
11990
12268
  }
12269
+ function validateAnimation(spec, errors) {
12270
+ const animation = spec.animation;
12271
+ if (!animation || typeof animation !== "object" || Array.isArray(animation)) return;
12272
+ const update = animation.update;
12273
+ if (!update || typeof update !== "object" || Array.isArray(update)) return;
12274
+ const maxMarks = update.maxMarks;
12275
+ if (maxMarks === void 0) return;
12276
+ if (typeof maxMarks !== "number" || !Number.isFinite(maxMarks) || maxMarks < 1) {
12277
+ errors.push({
12278
+ message: `Spec error: animation.update.maxMarks must be a number >= 1, got ${JSON.stringify(maxMarks)}`,
12279
+ path: "animation.update.maxMarks",
12280
+ code: "INVALID_VALUE",
12281
+ suggestion: "Set animation.update.maxMarks to the largest mark count that should still tween, e.g. animation: { update: { maxMarks: 5000 } }. Omit it to use the default of 500."
12282
+ });
12283
+ }
12284
+ }
11991
12285
  function validateSpec(spec) {
11992
12286
  const errors = [];
11993
12287
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
@@ -12096,6 +12390,9 @@ function validateSpec(spec) {
12096
12390
  } else if (isBarList) {
12097
12391
  validateBarListSpec(obj, errors);
12098
12392
  }
12393
+ if (!isGraph) {
12394
+ validateAnimation(obj, errors);
12395
+ }
12099
12396
  if (errors.length > 0) {
12100
12397
  return { valid: false, errors, normalized: null };
12101
12398
  }
@@ -12119,6 +12416,36 @@ ${errorMessages}`);
12119
12416
  return { spec: normalized, warnings };
12120
12417
  }
12121
12418
 
12419
+ // src/layout/shared.ts
12420
+ function chromeToInput(chrome) {
12421
+ return {
12422
+ eyebrow: chrome.eyebrow,
12423
+ title: chrome.title,
12424
+ subtitle: chrome.subtitle,
12425
+ source: chrome.source,
12426
+ byline: chrome.byline,
12427
+ footer: chrome.footer,
12428
+ brand: chrome.brand
12429
+ };
12430
+ }
12431
+ function scalePadding(basePadding, width, height) {
12432
+ const minDim = Math.min(width, height);
12433
+ if (minDim >= 500) return basePadding;
12434
+ if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
12435
+ const t = (minDim - 200) / 300;
12436
+ return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
12437
+ }
12438
+ function bottomMargin(bottomHeight, padding, xAxisHeight) {
12439
+ return padding + bottomHeight + xAxisHeight;
12440
+ }
12441
+ var INLINE_TICK_OVERHANG_PAD = 6;
12442
+ function resolveChromeLayout(spec, options) {
12443
+ const encoding = spec?.encoding;
12444
+ const isFaceted = !!(encoding?.facet || encoding?.row || encoding?.column);
12445
+ if (isFaceted) return "subtract";
12446
+ return spec?.chromeLayout ?? options?.chromeLayout ?? "subtract";
12447
+ }
12448
+
12122
12449
  // src/barlist/compile-barlist.ts
12123
12450
  var DEFAULT_ROW_GAP = 8;
12124
12451
  var LABEL_BAR_GAP = 12;
@@ -12166,14 +12493,19 @@ function compileBarList(spec, options) {
12166
12493
  watermark
12167
12494
  );
12168
12495
  const padding = theme.spacing.padding;
12496
+ const chromeLayout = resolveChromeLayout(
12497
+ spec,
12498
+ options
12499
+ );
12500
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
12169
12501
  const fullArea = {
12170
12502
  x: padding,
12171
12503
  y: padding + chrome.topHeight,
12172
12504
  width: options.width - padding * 2,
12173
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12505
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12174
12506
  };
12175
12507
  if (fullArea.width <= 0 || fullArea.height <= 0) {
12176
- return emptyLayout2(chrome, theme, options, watermark);
12508
+ return emptyLayout2(chrome, theme, { ...options, height: grownHeight }, watermark);
12177
12509
  }
12178
12510
  const labelField = barlistSpec.encoding.label.field;
12179
12511
  const valueField = barlistSpec.encoding.value.field;
@@ -12358,7 +12690,7 @@ function compileBarList(spec, options) {
12358
12690
  a11y,
12359
12691
  theme,
12360
12692
  width: options.width,
12361
- height: options.height,
12693
+ height: grownHeight,
12362
12694
  animation: resolvedAnimation,
12363
12695
  watermark,
12364
12696
  measureText
@@ -13650,7 +13982,8 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
13650
13982
  fill: theme.colors.text,
13651
13983
  lineHeight: 1.3
13652
13984
  };
13653
- const resolvedPosition = spec.legend?.position ?? (strategy.legendPosition === "right" ? "right" : "top");
13985
+ const specPosition = spec.legend?.position === "top-left" ? "top" : spec.legend?.position;
13986
+ const resolvedPosition = specPosition ?? (strategy.legendPosition === "right" ? "right" : "top");
13654
13987
  if (entries.length === 0) {
13655
13988
  return emptyContent(resolvedPosition, theme);
13656
13989
  }
@@ -14651,30 +14984,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
14651
14984
  return result;
14652
14985
  }
14653
14986
 
14654
- // src/layout/shared.ts
14655
- function chromeToInput(chrome) {
14656
- return {
14657
- eyebrow: chrome.eyebrow,
14658
- title: chrome.title,
14659
- subtitle: chrome.subtitle,
14660
- source: chrome.source,
14661
- byline: chrome.byline,
14662
- footer: chrome.footer,
14663
- brand: chrome.brand
14664
- };
14665
- }
14666
- function scalePadding(basePadding, width, height) {
14667
- const minDim = Math.min(width, height);
14668
- if (minDim >= 500) return basePadding;
14669
- if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
14670
- const t = (minDim - 200) / 300;
14671
- return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
14672
- }
14673
- function bottomMargin(bottomHeight, padding, xAxisHeight) {
14674
- return padding + bottomHeight + xAxisHeight;
14675
- }
14676
- var INLINE_TICK_OVERHANG_PAD = 6;
14677
-
14678
14987
  // src/layout/plan.ts
14679
14988
  function createMeasureFn(measureText) {
14680
14989
  if (measureText) {
@@ -14896,7 +15205,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
14896
15205
  if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
14897
15206
  topMargin += effectiveTopAxisGap;
14898
15207
  const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
14899
- const chartHeight = Math.max(0, height - topMargin - bMargin);
15208
+ const chromeLayout = resolveChromeLayout(renderSpec, options);
15209
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
15210
+ const chartHeight = Math.max(0, effectiveHeight - topMargin - bMargin);
14900
15211
  const rightMargin = hPad + (isRadial ? hPad : axisMargin);
14901
15212
  const rightAxisReserve = options.rightAxisReserve ?? 0;
14902
15213
  const provisionalArea = {
@@ -15048,11 +15359,12 @@ function compileLayer(spec, options, compileChart2) {
15048
15359
  const singleSpec = buildPrimarySpec(leaves, spec);
15049
15360
  return compileChart2(singleSpec, options);
15050
15361
  }
15362
+ const layerOptions = { ...options, layered: true };
15051
15363
  if (spec.resolve?.scale?.y === "independent") {
15052
- return compileLayerIndependent(leaves, spec, options, compileChart2);
15364
+ return compileLayerIndependent(leaves, spec, layerOptions, compileChart2);
15053
15365
  }
15054
15366
  const primarySpec = buildPrimarySpec(leaves, spec);
15055
- const primaryLayout = compileChart2(primarySpec, options);
15367
+ const primaryLayout = compileChart2(primarySpec, layerOptions);
15056
15368
  const allMarks = [];
15057
15369
  const seenLabels = /* @__PURE__ */ new Set();
15058
15370
  const pLegend = primaryLayout.legend;
@@ -15065,7 +15377,7 @@ function compileLayer(spec, options, compileChart2) {
15065
15377
  zIndex: leaf.zIndex ?? i
15066
15378
  }));
15067
15379
  indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
15068
- const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
15380
+ const leafOptions = { ...layerOptions, frozenChartArea: primaryLayout.area };
15069
15381
  const sharedDomains = computeSharedDomains(leaves);
15070
15382
  for (const { leaf } of indexedLeaves) {
15071
15383
  const themedLeaf = {
@@ -15174,11 +15486,9 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
15174
15486
  };
15175
15487
  const xField0 = leaf0.encoding?.x?.field;
15176
15488
  const xField1 = leaf1.encoding?.x?.field;
15177
- const unionXValues = /* @__PURE__ */ new Set();
15178
- if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
15179
- if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
15180
- let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
15181
- let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
15489
+ const union = withUnionXDomain(leaf0, leaf1);
15490
+ let leaf0WithUnionX = union.leaf0;
15491
+ let leaf1WithUnionX = union.leaf1;
15182
15492
  const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
15183
15493
  if (aligned) {
15184
15494
  leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
@@ -15325,20 +15635,75 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
15325
15635
  tooltipDescriptors: mergedTooltips
15326
15636
  };
15327
15637
  }
15328
- function ensureXDomainCoverage(leaf, xField, allXValues) {
15329
- if (!xField || allXValues.size === 0) return leaf;
15330
- const existingXValues = /* @__PURE__ */ new Set();
15331
- for (const row of leaf.data) existingXValues.add(row[xField]);
15332
- const missingRows = [];
15333
- for (const xVal of allXValues) {
15334
- if (!existingXValues.has(xVal)) {
15335
- missingRows.push({ [xField]: xVal });
15638
+ function withXDomain(leaf, domain) {
15639
+ if (!leaf.encoding?.x) return leaf;
15640
+ return {
15641
+ ...leaf,
15642
+ encoding: {
15643
+ ...leaf.encoding,
15644
+ x: {
15645
+ ...leaf.encoding.x,
15646
+ scale: {
15647
+ ...leaf.encoding.x.scale,
15648
+ domain
15649
+ }
15650
+ }
15651
+ }
15652
+ };
15653
+ }
15654
+ function withUnionXDomain(leaf0, leaf1) {
15655
+ const xEnc0 = leaf0.encoding?.x;
15656
+ const xEnc1 = leaf1.encoding?.x;
15657
+ const xField0 = xEnc0?.field;
15658
+ const xField1 = xEnc1?.field;
15659
+ if (!xField0 || !xField1) return { leaf0, leaf1 };
15660
+ const xType = xEnc0?.type ?? xEnc1?.type;
15661
+ const pinned0 = xEnc0?.scale?.domain !== void 0;
15662
+ const pinned1 = xEnc1?.scale?.domain !== void 0;
15663
+ if (pinned0 && pinned1) return { leaf0, leaf1 };
15664
+ const values0 = leaf0.data.map((row) => row[xField0]);
15665
+ const values1 = leaf1.data.map((row) => row[xField1]);
15666
+ const allValues = [...values0, ...values1].filter((v) => v != null);
15667
+ if (allValues.length === 0) return { leaf0, leaf1 };
15668
+ let domain;
15669
+ if (xType === "temporal") {
15670
+ let minMs = Number.POSITIVE_INFINITY;
15671
+ let maxMs = Number.NEGATIVE_INFINITY;
15672
+ for (const v of allValues) {
15673
+ const ms = (v instanceof Date ? v : new Date(String(v))).getTime();
15674
+ if (Number.isNaN(ms)) continue;
15675
+ if (ms < minMs) minMs = ms;
15676
+ if (ms > maxMs) maxMs = ms;
15677
+ }
15678
+ if (minMs <= maxMs) {
15679
+ domain = [new Date(minMs).toISOString(), new Date(maxMs).toISOString()];
15680
+ }
15681
+ } else if (xType === "quantitative") {
15682
+ let min4 = Number.POSITIVE_INFINITY;
15683
+ let max4 = Number.NEGATIVE_INFINITY;
15684
+ for (const v of allValues) {
15685
+ const n = typeof v === "number" ? v : Number(v);
15686
+ if (!Number.isFinite(n)) continue;
15687
+ if (n < min4) min4 = n;
15688
+ if (n > max4) max4 = n;
15689
+ }
15690
+ if (min4 <= max4) domain = [min4, max4];
15691
+ } else {
15692
+ const seen = /* @__PURE__ */ new Set();
15693
+ const categories = [];
15694
+ for (const v of allValues) {
15695
+ const s = String(v);
15696
+ if (!seen.has(s)) {
15697
+ seen.add(s);
15698
+ categories.push(s);
15699
+ }
15336
15700
  }
15701
+ if (categories.length > 0) domain = categories;
15337
15702
  }
15338
- if (missingRows.length === 0) return leaf;
15703
+ if (domain === void 0) return { leaf0, leaf1 };
15339
15704
  return {
15340
- ...leaf,
15341
- data: [...leaf.data, ...missingRows]
15705
+ leaf0: pinned0 ? leaf0 : withXDomain(leaf0, domain),
15706
+ leaf1: pinned1 ? leaf1 : withXDomain(leaf1, domain)
15342
15707
  };
15343
15708
  }
15344
15709
  function alignYDomains(leaf0, leaf1) {
@@ -15457,6 +15822,10 @@ function buildPrimarySpec(leaves, layerSpec) {
15457
15822
  ...leaves[0],
15458
15823
  data: allData,
15459
15824
  chrome: layerSpec.chrome ?? leaves[0].chrome,
15825
+ // Annotations come from the parent LayerSpec when present, otherwise
15826
+ // the first flattened leaf's. Later leaves' annotations are silently
15827
+ // dropped. The dual-axis path (compileLayerIndependent) explicitly
15828
+ // clears the secondary axis spec's annotations.
15460
15829
  annotations: layerSpec.annotations ?? leaves[0].annotations,
15461
15830
  labels: layerSpec.labels ?? leaves[0].labels,
15462
15831
  legend: layerSpec.legend ?? leaves[0].legend,
@@ -15613,6 +15982,34 @@ function collectContrastWarnings(scales, markType, theme) {
15613
15982
  return warnings;
15614
15983
  }
15615
15984
 
15985
+ // src/compiler/mark-render-mode.ts
15986
+ var AUTO_CANVAS_THRESHOLD = 1e3;
15987
+ var AUTO_ENABLED = true;
15988
+ function resolveMarkRenderMode(args, warnings = []) {
15989
+ const { markDef, markType, pointCount, display, faceted, layered } = args;
15990
+ const requested = markDef?.render ?? "auto";
15991
+ if (requested === "svg") return "svg";
15992
+ const refusal = describeRefusal(markType, display, faceted, layered);
15993
+ if (refusal) {
15994
+ if (requested === "canvas") {
15995
+ warnings.push(
15996
+ `Chart warning: mark.render "canvas" is not supported ${refusal}; rendering marks as SVG instead.`
15997
+ );
15998
+ }
15999
+ return "svg";
16000
+ }
16001
+ if (requested === "canvas") return "canvas";
16002
+ if (!AUTO_ENABLED) return "svg";
16003
+ return pointCount > AUTO_CANVAS_THRESHOLD ? "canvas" : "svg";
16004
+ }
16005
+ function describeRefusal(markType, display, faceted, layered) {
16006
+ if (markType !== "point") return `for ${markType} marks (point marks only)`;
16007
+ if (faceted) return "on faceted charts";
16008
+ if (layered) return "on layered charts";
16009
+ if (display === "sparkline") return "on sparklines";
16010
+ return void 0;
16011
+ }
16012
+
15616
16013
  // src/compiler/sparkline-defaults.ts
15617
16014
  var TREND_DEADBAND = 5e-4;
15618
16015
  function computeTrend(values) {
@@ -15691,7 +16088,7 @@ import {
15691
16088
  HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
15692
16089
  HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
15693
16090
  resolveTheme as resolveTheme3,
15694
- SEQUENTIAL_PALETTES
16091
+ SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
15695
16092
  } from "@opendata-ai/openchart-core";
15696
16093
 
15697
16094
  // ../../node_modules/.bun/d3-geo@3.1.1/node_modules/d3-geo/src/math.js
@@ -17669,13 +18066,32 @@ function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
17669
18066
 
17670
18067
  // src/geo/projections.ts
17671
18068
  var MERCATOR_FIT_MIN_LAT = -60;
17672
- function createProjection(type, width, height, geojson) {
18069
+ function createProjection(type, width, height, geojson, inset = 0) {
17673
18070
  if (type === "identity") {
18071
+ if (inset > 0) {
18072
+ return identity_default4().fitExtent(
18073
+ [
18074
+ [inset, inset],
18075
+ [width - inset, height - inset]
18076
+ ],
18077
+ geojson
18078
+ );
18079
+ }
17674
18080
  return identity_default4().fitSize([width, height], geojson);
17675
18081
  }
17676
18082
  const projection2 = type === "albersUsa" ? albersUsa_default() : type === "equalEarth" ? equalEarth_default() : mercator_default();
17677
18083
  const fitGeometry = type === "mercator" ? excludePolarFeatures(geojson) : geojson;
17678
- projection2.fitSize([width, height], fitGeometry);
18084
+ if (inset > 0) {
18085
+ projection2.fitExtent(
18086
+ [
18087
+ [inset, inset],
18088
+ [width - inset, height - inset]
18089
+ ],
18090
+ fitGeometry
18091
+ );
18092
+ } else {
18093
+ projection2.fitSize([width, height], fitGeometry);
18094
+ }
17679
18095
  return projection2;
17680
18096
  }
17681
18097
  function excludePolarFeatures(geojson) {
@@ -17695,6 +18111,7 @@ function excludePolarFeatures(geojson) {
17695
18111
  }
17696
18112
 
17697
18113
  // src/geo/compile-map.ts
18114
+ var OVERLAY_LEGEND_INSET = 18;
17698
18115
  function validateGeoFeatures(geo) {
17699
18116
  if (!geo.features) {
17700
18117
  throw new Error(
@@ -17752,11 +18169,15 @@ function compileMap(spec, options) {
17752
18169
  watermark
17753
18170
  );
17754
18171
  const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
18172
+ const chromeLayout = resolveChromeLayout(
18173
+ spec,
18174
+ options
18175
+ );
17755
18176
  const fullArea = {
17756
18177
  x: padding,
17757
18178
  y: padding + chrome.topHeight,
17758
18179
  width: options.width - padding * 2,
17759
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
18180
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
17760
18181
  };
17761
18182
  if (fullArea.width <= 0 || fullArea.height <= 0) {
17762
18183
  return emptyLayout3(chrome, theme, options, watermark);
@@ -17767,11 +18188,13 @@ function compileMap(spec, options) {
17767
18188
  const compileWarnings = [];
17768
18189
  const showLegend = mapSpec.legend?.show !== false;
17769
18190
  const colorEncoding = mapSpec.encoding.color;
17770
- const isQuantitative = colorEncoding.type === "quantitative";
18191
+ const showChoroplethLegend = showLegend && colorEncoding?.legend !== null;
18192
+ const isQuantitative = colorEncoding?.type === "quantitative";
17771
18193
  const legendPosition = mapSpec.legend?.position === "bottom" ? "bottom" : "top";
18194
+ const pointLegendOverlay = mapSpec.legend?.position === "top-left";
17772
18195
  let legendBlockHeight = 0;
17773
18196
  let continuousContent = null;
17774
- if (showLegend && isQuantitative) {
18197
+ if (showChoroplethLegend && colorEncoding && isQuantitative) {
17775
18198
  const colorValues = [];
17776
18199
  for (const row of mapSpec.data) {
17777
18200
  const v = row[colorEncoding.field];
@@ -17794,17 +18217,37 @@ function compileMap(spec, options) {
17794
18217
  const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
17795
18218
  legendBlockHeight = continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
17796
18219
  }
17797
- } else if (showLegend) {
18220
+ } else if (showChoroplethLegend && colorEncoding) {
17798
18221
  const labelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
17799
18222
  legendBlockHeight = Math.max(10, labelHeight) + 6;
17800
18223
  }
18224
+ if (showLegend && !pointLegendOverlay && mapSpec.points?.color) {
18225
+ if (mapSpec.points.color.type === "quantitative") {
18226
+ const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
18227
+ legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
18228
+ } else {
18229
+ const ptLabelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
18230
+ legendBlockHeight += Math.max(10, ptLabelHeight) + 6 + 8;
18231
+ }
18232
+ }
17801
18233
  const legendReserveGap = legendBlockHeight > 0 ? 8 : 0;
17802
18234
  const mapAreaHeight = fullArea.height - legendBlockHeight - legendReserveGap;
17803
18235
  if (mapAreaHeight <= 0) {
17804
18236
  return emptyLayout3(chrome, theme, options, watermark);
17805
18237
  }
17806
18238
  const projectionType = mapSpec.geo.projection;
17807
- const projection2 = createProjection(projectionType, fullArea.width, mapAreaHeight, geoCollection);
18239
+ let pointInset = 0;
18240
+ if (mapSpec.points) {
18241
+ const sizeRange = mapSpec.points.size?.scale?.range;
18242
+ pointInset = sizeRange ? sizeRange[1] : SIZE_SCALE_DEFAULTS.mapPoint.range[1];
18243
+ }
18244
+ const projection2 = createProjection(
18245
+ projectionType,
18246
+ fullArea.width,
18247
+ mapAreaHeight,
18248
+ geoCollection,
18249
+ pointInset
18250
+ );
17808
18251
  const pathGen = path_default(projection2);
17809
18252
  if (projectionType !== "identity") {
17810
18253
  for (const feat of geoFeatures) {
@@ -17819,31 +18262,44 @@ function compileMap(spec, options) {
17819
18262
  }
17820
18263
  }
17821
18264
  }
17822
- const featureObjs = geoFeatures.map((f) => ({
17823
- id: f.id,
17824
- properties: f.properties ?? {}
17825
- }));
17826
- const { joined, warnings: joinWarnings } = joinDataToFeatures(
17827
- featureObjs,
17828
- mapSpec.data,
17829
- mapSpec.encoding.key.field,
17830
- mapSpec.geo.idField
17831
- );
17832
- compileWarnings.push(...joinWarnings);
18265
+ let joined = /* @__PURE__ */ new Map();
18266
+ if (mapSpec.encoding.key) {
18267
+ const featureObjs = geoFeatures.map((f) => ({
18268
+ id: f.id,
18269
+ properties: f.properties ?? {}
18270
+ }));
18271
+ const joinResult = joinDataToFeatures(
18272
+ featureObjs,
18273
+ mapSpec.data,
18274
+ mapSpec.encoding.key.field,
18275
+ mapSpec.geo.idField
18276
+ );
18277
+ joined = joinResult.joined;
18278
+ const filteredJoinWarnings = mapSpec.data.length === 0 && mapSpec.points ? joinResult.warnings.filter((w) => w.code !== "UNMATCHED_FEATURES") : joinResult.warnings;
18279
+ compileWarnings.push(...filteredJoinWarnings);
18280
+ }
17833
18281
  const droppedFeatures = [];
17834
- const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
18282
+ const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber3;
17835
18283
  const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
17836
18284
  const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
17837
18285
  let featureMarks;
17838
18286
  let continuousLegend = null;
17839
18287
  let categoricalLegend = null;
17840
- if (isQuantitative) {
18288
+ if (!colorEncoding) {
18289
+ featureMarks = buildBasemapMarks({
18290
+ geoFeatures,
18291
+ pathGen,
18292
+ neutralFill,
18293
+ neutralStroke,
18294
+ droppedFeatures
18295
+ });
18296
+ } else if (isQuantitative) {
17841
18297
  const result = buildQuantitativeMarks({
17842
18298
  geoFeatures,
17843
18299
  pathGen,
17844
18300
  joined,
17845
18301
  colorField: colorEncoding.field,
17846
- palette: mapSpec.encoding.color.scale?.scheme ?? "blue",
18302
+ palette: colorEncoding.scale?.scheme ?? "blue",
17847
18303
  isDarkMode: !!isDarkMode,
17848
18304
  neutralFill,
17849
18305
  neutralStroke,
@@ -17851,7 +18307,7 @@ function compileMap(spec, options) {
17851
18307
  droppedFeatures
17852
18308
  });
17853
18309
  featureMarks = result.marks;
17854
- if (showLegend && continuousContent) {
18310
+ if (showChoroplethLegend && continuousContent) {
17855
18311
  const labelStyle = {
17856
18312
  fontFamily: theme.fonts.family,
17857
18313
  fontSize: theme.fonts.sizes.small,
@@ -17893,6 +18349,7 @@ function compileMap(spec, options) {
17893
18349
  pathGen,
17894
18350
  joined,
17895
18351
  colorField: colorEncoding.field,
18352
+ colorScale: colorEncoding.scale,
17896
18353
  isDarkMode: !!isDarkMode,
17897
18354
  neutralFill,
17898
18355
  neutralStroke,
@@ -17900,7 +18357,8 @@ function compileMap(spec, options) {
17900
18357
  fullArea,
17901
18358
  mapAreaHeight,
17902
18359
  legendReserveGap,
17903
- showLegend,
18360
+ legendPosition,
18361
+ showLegend: showChoroplethLegend,
17904
18362
  droppedFeatures
17905
18363
  });
17906
18364
  featureMarks = result.marks;
@@ -17928,6 +18386,201 @@ function compileMap(spec, options) {
17928
18386
  const interiorStroke = isDarkMode ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.15)";
17929
18387
  const outlineStroke = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.3)";
17930
18388
  const borders = { interiorPath, outlinePath, interiorStroke, outlineStroke };
18389
+ const pointMarks = [];
18390
+ let pointCategoricalLegend = null;
18391
+ let pointContinuousLegend = null;
18392
+ if (mapSpec.points) {
18393
+ const pts = mapSpec.points;
18394
+ const lonField = pts.longitude.field;
18395
+ const latField = pts.latitude.field;
18396
+ const keyChannel = pts.key;
18397
+ const sizeScale = buildSizeScale(pts.size, pts.data, SIZE_SCALE_DEFAULTS.mapPoint);
18398
+ let pointColorScale = null;
18399
+ const pointCategories = [];
18400
+ const pointCategoryColors = /* @__PURE__ */ new Map();
18401
+ if (pts.color) {
18402
+ if (pts.color.type === "quantitative") {
18403
+ const colorValues = [];
18404
+ for (const row of pts.data) {
18405
+ const raw = row[pts.color.field];
18406
+ if (raw != null) {
18407
+ const v = Number(raw);
18408
+ if (Number.isFinite(v)) colorValues.push(v);
18409
+ }
18410
+ }
18411
+ if (colorValues.length > 0) {
18412
+ const palette = pts.color.scale?.scheme ?? "blue";
18413
+ const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
18414
+ const qScale = quantile2().domain(colorValues).range(paletteStops);
18415
+ pointColorScale = (val) => {
18416
+ const v = Number(val);
18417
+ return Number.isFinite(v) ? qScale(v) : CATEGORICAL_PALETTE[0];
18418
+ };
18419
+ }
18420
+ } else {
18421
+ const explicitDomain = pts.color.scale?.domain;
18422
+ const explicitRange = pts.color.scale?.range;
18423
+ if (explicitDomain && Array.isArray(explicitDomain)) {
18424
+ for (const cat of explicitDomain) {
18425
+ pointCategories.push(String(cat));
18426
+ }
18427
+ }
18428
+ const seen = new Set(pointCategories);
18429
+ for (const row of pts.data) {
18430
+ const raw = row[pts.color.field];
18431
+ if (raw != null) {
18432
+ const cat = String(raw);
18433
+ if (!seen.has(cat)) {
18434
+ seen.add(cat);
18435
+ pointCategories.push(cat);
18436
+ }
18437
+ }
18438
+ }
18439
+ const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
18440
+ for (let i = 0; i < pointCategories.length; i++) {
18441
+ pointCategoryColors.set(pointCategories[i], colorSource[i % colorSource.length]);
18442
+ }
18443
+ pointColorScale = (val) => {
18444
+ if (val == null) return colorSource[0];
18445
+ return pointCategoryColors.get(String(val)) ?? colorSource[0];
18446
+ };
18447
+ }
18448
+ }
18449
+ const defaultFill = CATEGORICAL_PALETTE[0];
18450
+ const pointOpacity = pts.opacity ?? 0.65;
18451
+ let animIndex = 0;
18452
+ const droppedPoints = [];
18453
+ for (let i = 0; i < pts.data.length; i++) {
18454
+ const row = pts.data[i];
18455
+ const lon = Number(row[lonField]);
18456
+ const lat = Number(row[latField]);
18457
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
18458
+ const projected = projection2([lon, lat]);
18459
+ if (!projected) {
18460
+ droppedPoints.push([lon, lat]);
18461
+ continue;
18462
+ }
18463
+ const r = sizeScale ? sizeScale.scale(Number(row[sizeScale.field])) : 5;
18464
+ const fill = pointColorScale && pts.color ? pointColorScale(row[pts.color.field]) : defaultFill;
18465
+ const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
18466
+ const stroke = isDarkMode ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.8)";
18467
+ pointMarks.push({
18468
+ type: "map-point",
18469
+ cx: projected[0],
18470
+ cy: projected[1],
18471
+ r,
18472
+ fill,
18473
+ stroke,
18474
+ strokeWidth: 1,
18475
+ fillOpacity: pointOpacity,
18476
+ key,
18477
+ data: row,
18478
+ aria: { role: "img", label: key },
18479
+ animationIndex: animIndex++
18480
+ });
18481
+ }
18482
+ if (droppedPoints.length > 0) {
18483
+ const sample = droppedPoints.slice(0, 3).map(([lo, la]) => `[${lo}, ${la}]`).join(", ");
18484
+ compileWarnings.push({
18485
+ code: "POINT_NULL_PROJECTION",
18486
+ message: `${droppedPoints.length} point(s) project to null and were dropped (e.g. ${sample}). This is common with albersUsa which excludes territories. Consider using projection: "mercator" or "equalEarth".`,
18487
+ context: { count: droppedPoints.length, samples: droppedPoints.slice(0, 5) }
18488
+ });
18489
+ }
18490
+ if (pts.color && pts.color.type !== "quantitative" && pointCategories.length > 0 && showLegend) {
18491
+ const labelStyle = {
18492
+ fontFamily: theme.fonts.family,
18493
+ fontSize: theme.fonts.sizes.small,
18494
+ fontWeight: theme.fonts.weights.normal,
18495
+ fill: theme.colors.text,
18496
+ lineHeight: 1.2
18497
+ };
18498
+ const entries = pointCategories.map((cat) => ({
18499
+ label: cat,
18500
+ color: pointCategoryColors.get(cat),
18501
+ shape: "circle"
18502
+ }));
18503
+ const bottomChoroplethHeight = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
18504
+ const choroplethLegendGap = bottomChoroplethHeight > 0 ? 8 : 0;
18505
+ const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
18506
+ const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethHeight + choroplethLegendGap;
18507
+ const legendWidth = pointLegendOverlay ? fullArea.width - OVERLAY_LEGEND_INSET * 2 : fullArea.width;
18508
+ const swatchSize = 10;
18509
+ const swatchGap = 6;
18510
+ const entryGap = 16;
18511
+ pointCategoricalLegend = {
18512
+ type: "categorical",
18513
+ position: pointLegendOverlay ? "top-left" : "bottom",
18514
+ bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
18515
+ labelStyle,
18516
+ entries,
18517
+ swatchSize,
18518
+ swatchGap,
18519
+ entryGap,
18520
+ swatchChipFill: isDarkMode ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"
18521
+ };
18522
+ }
18523
+ if (pts.color && pts.color.type === "quantitative" && showLegend) {
18524
+ const ptColorValues = [];
18525
+ for (const row of pts.data) {
18526
+ const v = row[pts.color.field];
18527
+ if (v != null) {
18528
+ const n = Number(v);
18529
+ if (Number.isFinite(n)) ptColorValues.push(n);
18530
+ }
18531
+ }
18532
+ const ptLegendChannel = {
18533
+ ...pts.color,
18534
+ scale: { ...pts.color.scale, type: "quantile" }
18535
+ };
18536
+ const ptContinuousContent = computeContinuousLegendContentForChannel(
18537
+ ptColorValues,
18538
+ ptLegendChannel,
18539
+ theme,
18540
+ fullArea.width
18541
+ );
18542
+ if (ptContinuousContent) {
18543
+ const labelStyle = {
18544
+ fontFamily: theme.fonts.family,
18545
+ fontSize: theme.fonts.sizes.small,
18546
+ fontWeight: theme.fonts.weights.normal,
18547
+ fill: theme.colors.text,
18548
+ lineHeight: 1.3,
18549
+ fontVariant: "tabular-nums"
18550
+ };
18551
+ const bottomChoroplethH = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
18552
+ const choroplethLegendGap = bottomChoroplethH > 0 ? 8 : 0;
18553
+ const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
18554
+ const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
18555
+ const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
18556
+ const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
18557
+ const bounds = {
18558
+ x: legendX,
18559
+ y: legendY,
18560
+ width: ptContinuousContent.barWidth,
18561
+ height: ptLegendHeight
18562
+ };
18563
+ const bar = {
18564
+ x: bounds.x,
18565
+ y: bounds.y,
18566
+ width: ptContinuousContent.barWidth,
18567
+ height: ptContinuousContent.barHeight
18568
+ };
18569
+ pointContinuousLegend = {
18570
+ type: "continuous",
18571
+ mode: ptContinuousContent.mode,
18572
+ position: pointLegendOverlay ? "top-left" : "bottom",
18573
+ bounds,
18574
+ labelStyle,
18575
+ bar,
18576
+ colorStops: ptContinuousContent.colorStops,
18577
+ bins: ptContinuousContent.bins.map((b) => ({ ...b, x: b.x + bar.x })),
18578
+ ticks: ptContinuousContent.ticks.map((t) => ({ ...t, x: t.x + bar.x })),
18579
+ labelY: bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize
18580
+ };
18581
+ }
18582
+ }
18583
+ }
17931
18584
  const tooltipChannels = mapSpec.encoding.tooltip ? Array.isArray(mapSpec.encoding.tooltip) ? mapSpec.encoding.tooltip : [mapSpec.encoding.tooltip] : null;
17932
18585
  const tooltipDescriptors = /* @__PURE__ */ new Map();
17933
18586
  for (const mark of featureMarks) {
@@ -17945,12 +18598,12 @@ function compileMap(spec, options) {
17945
18598
  });
17946
18599
  }
17947
18600
  }
17948
- } else if (isQuantitative && mark.data[colorEncoding.field] != null) {
18601
+ } else if (colorEncoding && isQuantitative && mark.data[colorEncoding.field] != null) {
17949
18602
  fields.push({
17950
18603
  label: colorEncoding.title ?? colorEncoding.field,
17951
18604
  value: formatter(Number(mark.data[colorEncoding.field]))
17952
18605
  });
17953
- } else if (!isQuantitative && mark.data[colorEncoding.field] != null) {
18606
+ } else if (colorEncoding && !isQuantitative && mark.data[colorEncoding.field] != null) {
17954
18607
  fields.push({
17955
18608
  label: colorEncoding.title ?? colorEncoding.field,
17956
18609
  value: String(mark.data[colorEncoding.field])
@@ -17958,13 +18611,63 @@ function compileMap(spec, options) {
17958
18611
  }
17959
18612
  tooltipDescriptors.set(String(mark.id), { fields });
17960
18613
  }
18614
+ if (mapSpec.points && pointMarks.length > 0) {
18615
+ const ptTooltipChannels = mapSpec.points.tooltip ? Array.isArray(mapSpec.points.tooltip) ? mapSpec.points.tooltip : [mapSpec.points.tooltip] : null;
18616
+ for (const pm of pointMarks) {
18617
+ const fields = [];
18618
+ if (ptTooltipChannels) {
18619
+ for (const ch of ptTooltipChannels) {
18620
+ const val = pm.data[ch.field];
18621
+ if (val != null) {
18622
+ fields.push({
18623
+ label: ch.title ?? ch.field,
18624
+ value: ch.type === "quantitative" ? formatter(Number(val)) : String(val)
18625
+ });
18626
+ }
18627
+ }
18628
+ } else {
18629
+ const pts = mapSpec.points;
18630
+ if (pts.key) {
18631
+ const val = pm.data[pts.key.field];
18632
+ if (val != null)
18633
+ fields.push({ label: pts.key.title ?? pts.key.field, value: String(val) });
18634
+ }
18635
+ if (pts.color) {
18636
+ const val = pm.data[pts.color.field];
18637
+ if (val != null)
18638
+ fields.push({
18639
+ label: pts.color.title ?? pts.color.field,
18640
+ value: pts.color.type === "quantitative" ? formatter(Number(val)) : String(val)
18641
+ });
18642
+ }
18643
+ if (pts.size) {
18644
+ const val = pm.data[pts.size.field];
18645
+ if (val != null)
18646
+ fields.push({ label: pts.size.title ?? pts.size.field, value: formatter(Number(val)) });
18647
+ }
18648
+ }
18649
+ if (fields.length > 0) {
18650
+ tooltipDescriptors.set(`point:${pm.key}`, { fields });
18651
+ }
18652
+ }
18653
+ }
18654
+ const a11yDataField = colorEncoding?.field;
17961
18655
  const a11y = {
17962
- altText: `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data`,
17963
- dataTableFallback: featureMarks.filter((m) => m.data).map((m) => [m.name ?? String(m.id), String(m.data?.[colorEncoding.field] ?? "")]),
18656
+ altText: pointMarks.length > 0 ? colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data and ${pointMarks.length} point markers` : `Map showing ${geoFeatures.length} regions with ${pointMarks.length} point markers` : colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data` : `Map showing ${geoFeatures.length} regions`,
18657
+ dataTableFallback: [
18658
+ ...featureMarks.filter((m) => m.data).map((m) => [
18659
+ m.name ?? String(m.id),
18660
+ a11yDataField ? String(m.data?.[a11yDataField] ?? "") : ""
18661
+ ]),
18662
+ ...pointMarks.map((pm) => [
18663
+ pm.key,
18664
+ mapSpec.points?.color ? String(pm.data[mapSpec.points.color.field] ?? "") : ""
18665
+ ])
18666
+ ],
17964
18667
  role: "img",
17965
- keyboardNavigable: featureMarks.length > 0
18668
+ keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
17966
18669
  };
17967
- const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
18670
+ const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, pointMarks, compileWarnings);
17968
18671
  const resolvedAnimation = resolveAnimation(mapSpec.animation);
17969
18672
  const mapAreaY = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
17970
18673
  const mapArea = {
@@ -17987,6 +18690,9 @@ function compileMap(spec, options) {
17987
18690
  borders,
17988
18691
  continuousLegend,
17989
18692
  categoricalLegend,
18693
+ pointMarks,
18694
+ pointCategoricalLegend,
18695
+ pointContinuousLegend,
17990
18696
  tooltipDescriptors,
17991
18697
  a11y,
17992
18698
  theme,
@@ -18000,6 +18706,46 @@ function compileMap(spec, options) {
18000
18706
  focus: resolvedFocus
18001
18707
  };
18002
18708
  }
18709
+ function buildBasemapMarks(opts) {
18710
+ const { geoFeatures, pathGen, neutralFill, neutralStroke, droppedFeatures } = opts;
18711
+ const marks = [];
18712
+ let animIndex = 0;
18713
+ for (const feat of geoFeatures) {
18714
+ const pathD = pathGen(feat);
18715
+ if (!pathD) {
18716
+ const name2 = feat.properties?.name ?? String(feat.id);
18717
+ droppedFeatures.push(name2);
18718
+ continue;
18719
+ }
18720
+ const featureId = feat.id;
18721
+ const props = feat.properties;
18722
+ const name = props?.name ?? props?.NAME;
18723
+ const b = pathGen.bounds(feat);
18724
+ const bx = Number.isFinite(b[0][0]) ? b[0][0] : 0;
18725
+ const by = Number.isFinite(b[0][1]) ? b[0][1] : 0;
18726
+ const bw = Number.isFinite(b[1][0]) ? b[1][0] - bx : 0;
18727
+ const bh = Number.isFinite(b[1][1]) ? b[1][1] - by : 0;
18728
+ const bounds = { x: bx, y: by, width: bw, height: bh };
18729
+ const rawCentroid = pathGen.centroid(feat);
18730
+ const cx = Number.isFinite(rawCentroid[0]) ? rawCentroid[0] : bx + bw / 2;
18731
+ const cy = Number.isFinite(rawCentroid[1]) ? rawCentroid[1] : by + bh / 2;
18732
+ marks.push({
18733
+ type: "map-feature",
18734
+ path: pathD,
18735
+ fill: neutralFill,
18736
+ stroke: neutralStroke,
18737
+ strokeWidth: 0.5,
18738
+ id: featureId,
18739
+ name,
18740
+ data: null,
18741
+ aria: { role: "img", label: name ?? String(featureId) },
18742
+ animationIndex: animIndex++,
18743
+ bounds,
18744
+ centroid: [cx, cy]
18745
+ });
18746
+ }
18747
+ return marks;
18748
+ }
18003
18749
  function buildQuantitativeMarks(opts) {
18004
18750
  const {
18005
18751
  geoFeatures,
@@ -18021,7 +18767,7 @@ function buildQuantitativeMarks(opts) {
18021
18767
  if (!Number.isNaN(v)) values.push(v);
18022
18768
  }
18023
18769
  }
18024
- const paletteStops = [...SEQUENTIAL_PALETTES[palette] ?? SEQUENTIAL_PALETTES.blue];
18770
+ const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
18025
18771
  const colorScale = quantile2().domain(values).range(paletteStops);
18026
18772
  const marks = [];
18027
18773
  let animIndex = 0;
@@ -18084,6 +18830,7 @@ function buildCategoricalMarks(opts) {
18084
18830
  pathGen,
18085
18831
  joined,
18086
18832
  colorField,
18833
+ colorScale: scaleConfig,
18087
18834
  isDarkMode,
18088
18835
  neutralFill,
18089
18836
  neutralStroke,
@@ -18091,11 +18838,18 @@ function buildCategoricalMarks(opts) {
18091
18838
  fullArea,
18092
18839
  mapAreaHeight,
18093
18840
  legendReserveGap,
18841
+ legendPosition,
18094
18842
  showLegend,
18095
18843
  droppedFeatures
18096
18844
  } = opts;
18097
18845
  const categories = [];
18098
- const seen = /* @__PURE__ */ new Set();
18846
+ const explicitDomain = scaleConfig?.domain;
18847
+ if (explicitDomain && Array.isArray(explicitDomain)) {
18848
+ for (const cat of explicitDomain) {
18849
+ categories.push(String(cat));
18850
+ }
18851
+ }
18852
+ const seen = new Set(categories);
18099
18853
  for (const row of joined.values()) {
18100
18854
  const raw = row[colorField];
18101
18855
  if (raw != null) {
@@ -18106,9 +18860,11 @@ function buildCategoricalMarks(opts) {
18106
18860
  }
18107
18861
  }
18108
18862
  }
18863
+ const explicitRange = scaleConfig?.range;
18864
+ const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
18109
18865
  const categoryColors = /* @__PURE__ */ new Map();
18110
18866
  for (let i = 0; i < categories.length; i++) {
18111
- categoryColors.set(categories[i], CATEGORICAL_PALETTE[i % CATEGORICAL_PALETTE.length]);
18867
+ categoryColors.set(categories[i], colorSource[i % colorSource.length]);
18112
18868
  }
18113
18869
  const marks = [];
18114
18870
  let animIndex = 0;
@@ -18176,14 +18932,14 @@ function buildCategoricalMarks(opts) {
18176
18932
  shape: "square"
18177
18933
  }));
18178
18934
  const legendX = fullArea.x;
18179
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap;
18935
+ const legendY = legendPosition === "bottom" ? fullArea.y + mapAreaHeight + legendReserveGap : fullArea.y + legendReserveGap;
18180
18936
  const legendWidth = fullArea.width;
18181
18937
  const swatchSize = 10;
18182
18938
  const swatchGap = 6;
18183
18939
  const entryGap = 16;
18184
18940
  legend = {
18185
18941
  type: "categorical",
18186
- position: "bottom",
18942
+ position: legendPosition,
18187
18943
  bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
18188
18944
  labelStyle,
18189
18945
  entries,
@@ -18195,8 +18951,40 @@ function buildCategoricalMarks(opts) {
18195
18951
  }
18196
18952
  return { marks, legend };
18197
18953
  }
18198
- function resolveFocus(focus, features, warnings) {
18954
+ function resolveFocus(focus, features, pointMarks, warnings) {
18199
18955
  if (focus === null || focus === void 0) return null;
18956
+ if (typeof focus === "object" && !Array.isArray(focus) && "points" in focus) {
18957
+ const filter = focus.points;
18958
+ const matched = filter === true ? pointMarks : pointMarks.filter((p) => p.data[filter.field] === filter.value);
18959
+ if (matched.length === 0) {
18960
+ const detail = filter === true ? "the map has no points to fit" : `no points match { field: "${filter.field}", value: ${JSON.stringify(filter.value)} }`;
18961
+ warnings.push({
18962
+ code: "FOCUS_UNMATCHED",
18963
+ message: `geo.focus.points is set but ${detail}`,
18964
+ context: {}
18965
+ });
18966
+ return null;
18967
+ }
18968
+ const padding2 = focus.padding ?? 16;
18969
+ let minX2 = Infinity;
18970
+ let minY2 = Infinity;
18971
+ let maxX2 = -Infinity;
18972
+ let maxY2 = -Infinity;
18973
+ for (const p of matched) {
18974
+ minX2 = Math.min(minX2, p.cx);
18975
+ minY2 = Math.min(minY2, p.cy);
18976
+ maxX2 = Math.max(maxX2, p.cx);
18977
+ maxY2 = Math.max(maxY2, p.cy);
18978
+ }
18979
+ const w = Math.max(maxX2 - minX2, 1);
18980
+ const h = Math.max(maxY2 - minY2, 1);
18981
+ return {
18982
+ target: { x: minX2, y: minY2, width: w, height: h, padding: padding2 },
18983
+ // No feature ids: focus-dim is a feature concept, so a points focus
18984
+ // dims no features (there are no "other" features to mute).
18985
+ ids: []
18986
+ };
18987
+ }
18200
18988
  let ids;
18201
18989
  let padding = 16;
18202
18990
  if (typeof focus === "string" || typeof focus === "number") {
@@ -18256,6 +19044,9 @@ function emptyLayout3(chrome, theme, options, watermark) {
18256
19044
  },
18257
19045
  continuousLegend: null,
18258
19046
  categoricalLegend: null,
19047
+ pointMarks: [],
19048
+ pointCategoricalLegend: null,
19049
+ pointContinuousLegend: null,
18259
19050
  tooltipDescriptors: /* @__PURE__ */ new Map(),
18260
19051
  a11y: {
18261
19052
  altText: "Empty map",
@@ -18283,6 +19074,65 @@ import {
18283
19074
  resolveTheme as resolveTheme4
18284
19075
  } from "@opendata-ai/openchart-core";
18285
19076
 
19077
+ // src/graphs/animation.ts
19078
+ var ENTER_DEFAULT = {
19079
+ duration: 500,
19080
+ ease: "smooth",
19081
+ stagger: true,
19082
+ cameraFit: true
19083
+ };
19084
+ var UPDATE_DEFAULT = { duration: 300, ease: "smooth" };
19085
+ var EXIT_DEFAULT = { duration: 300, ease: "smooth" };
19086
+ var CAMERA_DEFAULT = { duration: "auto", ease: "smooth" };
19087
+ var HOVER_DEFAULT = { duration: 150, ease: "smooth" };
19088
+ function fullDefaults() {
19089
+ return {
19090
+ enter: { ...ENTER_DEFAULT },
19091
+ update: { ...UPDATE_DEFAULT },
19092
+ exit: { ...EXIT_DEFAULT },
19093
+ camera: { ...CAMERA_DEFAULT },
19094
+ hover: { ...HOVER_DEFAULT }
19095
+ };
19096
+ }
19097
+ function resolveGraphAnimation(spec) {
19098
+ if (spec === void 0 || spec === true) return fullDefaults();
19099
+ if (spec === false) return void 0;
19100
+ return {
19101
+ enter: resolveEnter(spec.enter),
19102
+ update: resolveDurationPhase(spec.update, UPDATE_DEFAULT),
19103
+ exit: resolveDurationPhase(spec.exit, EXIT_DEFAULT),
19104
+ camera: resolveCamera(spec.camera),
19105
+ hover: resolveDurationPhase(spec.hover, HOVER_DEFAULT)
19106
+ };
19107
+ }
19108
+ function resolveEnter(phase) {
19109
+ if (phase === false) return null;
19110
+ if (phase === void 0 || phase === true) return { ...ENTER_DEFAULT };
19111
+ return {
19112
+ duration: phase.duration ?? ENTER_DEFAULT.duration,
19113
+ ease: phase.ease ?? ENTER_DEFAULT.ease,
19114
+ stagger: phase.stagger !== false,
19115
+ cameraFit: ENTER_DEFAULT.cameraFit
19116
+ };
19117
+ }
19118
+ function resolveDurationPhase(phase, defaults) {
19119
+ if (phase === false) return null;
19120
+ if (phase === void 0 || phase === true)
19121
+ return { duration: defaults.duration, ease: defaults.ease };
19122
+ return {
19123
+ duration: phase.duration ?? defaults.duration,
19124
+ ease: phase.ease ?? defaults.ease
19125
+ };
19126
+ }
19127
+ function resolveCamera(phase) {
19128
+ if (phase === false) return null;
19129
+ if (phase === void 0 || phase === true) return { ...CAMERA_DEFAULT };
19130
+ return {
19131
+ duration: phase.duration ?? CAMERA_DEFAULT.duration,
19132
+ ease: phase.ease ?? CAMERA_DEFAULT.ease
19133
+ };
19134
+ }
19135
+
18286
19136
  // src/graphs/encoding.ts
18287
19137
  var DEFAULT_NODE_RADIUS = 5;
18288
19138
  var MIN_NODE_RADIUS = 3;
@@ -18322,16 +19172,42 @@ function computeDegrees(nodes, edges) {
18322
19172
  }
18323
19173
  return degrees2;
18324
19174
  }
19175
+ function resolveCategoricalDomain(values, sort, scaleDomain) {
19176
+ if (Array.isArray(scaleDomain) && scaleDomain.length > 0) {
19177
+ return scaleDomain.map((v) => String(v));
19178
+ }
19179
+ const seen = [];
19180
+ const set = /* @__PURE__ */ new Set();
19181
+ for (const v of values) {
19182
+ if (!set.has(v)) {
19183
+ set.add(v);
19184
+ seen.push(v);
19185
+ }
19186
+ }
19187
+ if (sort === null) return seen;
19188
+ if (Array.isArray(sort)) {
19189
+ const pinned = sort.filter((v) => set.has(v));
19190
+ const pinnedSet = new Set(pinned);
19191
+ const rest = seen.filter((v) => !pinnedSet.has(v));
19192
+ return [...pinned, ...rest];
19193
+ }
19194
+ const sorted = [...seen].sort((a, b) => a.localeCompare(b));
19195
+ return sort === "descending" ? sorted.reverse() : sorted;
19196
+ }
18325
19197
  function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18326
19198
  const degrees2 = computeDegrees(nodes, edges);
18327
19199
  const maxDegree = Math.max(1, ...degrees2.values());
18328
19200
  let sizeScale;
18329
19201
  if (encoding.nodeSize?.field) {
18330
19202
  const field = encoding.nodeSize.field;
19203
+ const scaleConfig = encoding.nodeSize.scale;
18331
19204
  const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
18332
19205
  const sizeMin = min(values) ?? 0;
18333
19206
  const sizeMax = max(values) ?? 1;
18334
- sizeScale = sqrt().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
19207
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [sizeMin, sizeMax];
19208
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [MIN_NODE_RADIUS, MAX_NODE_RADIUS];
19209
+ const ctor = scaleConfig?.type === "linear" ? linear2 : sqrt;
19210
+ sizeScale = ctor().domain(domain).range(range3);
18335
19211
  }
18336
19212
  let colorFn;
18337
19213
  if (encoding.nodeColor?.field) {
@@ -18352,13 +19228,39 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18352
19228
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
18353
19229
  };
18354
19230
  } else {
18355
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
19231
+ const domain = resolveCategoricalDomain(
19232
+ nodes.map((n) => String(n[field] ?? "")),
19233
+ encoding.nodeColor.sort,
19234
+ scaleConfig?.domain
19235
+ );
18356
19236
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18357
19237
  const ordinalScale = ordinal().domain(domain).range(range3);
18358
19238
  colorFn = (node) => ordinalScale(String(node[field] ?? ""));
18359
19239
  }
18360
19240
  }
18361
19241
  const defaultColor = theme.colors.categorical[0];
19242
+ let opacityScale;
19243
+ if (encoding.nodeOpacity?.field) {
19244
+ const field = encoding.nodeOpacity.field;
19245
+ const scaleConfig = encoding.nodeOpacity.scale;
19246
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19247
+ const opMin = min(values) ?? 0;
19248
+ const opMax = max(values) ?? 1;
19249
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [opMin, opMax];
19250
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0.25, 1];
19251
+ opacityScale = linear2().domain(domain).range(range3);
19252
+ }
19253
+ let labelPriorityScale;
19254
+ if (encoding.nodeLabelPriority?.field) {
19255
+ const field = encoding.nodeLabelPriority.field;
19256
+ const scaleConfig = encoding.nodeLabelPriority.scale;
19257
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19258
+ const lpMin = min(values) ?? 0;
19259
+ const lpMax = max(values) ?? 1;
19260
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [lpMin, lpMax];
19261
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0, 1];
19262
+ labelPriorityScale = linear2().domain(domain).range(range3);
19263
+ }
18362
19264
  return nodes.map((node) => {
18363
19265
  let radius = DEFAULT_NODE_RADIUS;
18364
19266
  if (sizeScale && encoding.nodeSize?.field) {
@@ -18367,6 +19269,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18367
19269
  radius = sizeScale(val);
18368
19270
  }
18369
19271
  }
19272
+ let opacity = 1;
19273
+ if (opacityScale && encoding.nodeOpacity?.field) {
19274
+ const val = Number(node[encoding.nodeOpacity.field]);
19275
+ if (Number.isFinite(val)) {
19276
+ opacity = opacityScale(val);
19277
+ }
19278
+ }
18370
19279
  const fill = colorFn ? colorFn(node) : defaultColor;
18371
19280
  const stroke = darkenColor(fill);
18372
19281
  let label;
@@ -18376,8 +19285,14 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18376
19285
  } else {
18377
19286
  label = node.id;
18378
19287
  }
18379
- const degree = degrees2.get(node.id) ?? 0;
18380
- const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19288
+ let labelPriority;
19289
+ if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
19290
+ const val = Number(node[encoding.nodeLabelPriority.field]);
19291
+ labelPriority = Number.isFinite(val) ? labelPriorityScale(val) : 0;
19292
+ } else {
19293
+ const degree = degrees2.get(node.id) ?? 0;
19294
+ labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19295
+ }
18381
19296
  const { id: _id, ...rest } = node;
18382
19297
  const data = { id: node.id, ...rest };
18383
19298
  const override = nodeOverrides?.[node.id];
@@ -18395,6 +19310,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18395
19310
  label,
18396
19311
  labelPriority: finalLabelPriority,
18397
19312
  community: void 0,
19313
+ opacity,
18398
19314
  data
18399
19315
  };
18400
19316
  });
@@ -18427,7 +19343,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
18427
19343
  return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
18428
19344
  };
18429
19345
  } else {
18430
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
19346
+ const domain = resolveCategoricalDomain(
19347
+ edges.map((e) => String(e[field] ?? "")),
19348
+ encoding.edgeColor.sort,
19349
+ scaleConfig?.domain
19350
+ );
18431
19351
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18432
19352
  const ordinalScale = ordinal().domain(domain).range(range3);
18433
19353
  edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
@@ -18438,10 +19358,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
18438
19358
  let styleFn;
18439
19359
  if (encoding.edgeStyle?.field) {
18440
19360
  const field = encoding.edgeStyle.field;
18441
- const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
19361
+ const domain = resolveCategoricalDomain(
19362
+ edges.map((e) => String(e[field] ?? "")),
19363
+ encoding.edgeStyle.sort,
19364
+ encoding.edgeStyle.scale?.domain
19365
+ );
18442
19366
  const styleMap = /* @__PURE__ */ new Map();
18443
- for (let i = 0; i < uniqueValues.length; i++) {
18444
- styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19367
+ for (let i = 0; i < domain.length; i++) {
19368
+ styleMap.set(domain[i], EDGE_STYLES[i % EDGE_STYLES.length]);
18445
19369
  }
18446
19370
  styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
18447
19371
  }
@@ -18499,12 +19423,49 @@ function applyCommunityColors(nodes, colorMap) {
18499
19423
  }
18500
19424
  }
18501
19425
 
19426
+ // src/graphs/interaction.ts
19427
+ var DEFAULT_DIM_OPACITY = 0.15;
19428
+ var DEFAULT_CURSOR_RADIUS = 80;
19429
+ var DEFAULT_CURSOR_STRENGTH = 30;
19430
+ function resolveGraphInteraction(cfg) {
19431
+ const hover = cfg?.hover;
19432
+ const cursor = cfg?.cursorRepulsion;
19433
+ let cursorRepulsion = null;
19434
+ if (cursor === true) {
19435
+ cursorRepulsion = { radius: DEFAULT_CURSOR_RADIUS, strength: DEFAULT_CURSOR_STRENGTH };
19436
+ } else if (cursor && typeof cursor === "object") {
19437
+ cursorRepulsion = {
19438
+ radius: cursor.radius ?? DEFAULT_CURSOR_RADIUS,
19439
+ strength: cursor.strength ?? DEFAULT_CURSOR_STRENGTH
19440
+ };
19441
+ }
19442
+ return {
19443
+ hoverMode: hover?.mode ?? "neighbors",
19444
+ dimOpacity: hover?.dimOpacity ?? DEFAULT_DIM_OPACITY,
19445
+ selectFlyTo: cfg?.select?.flyTo ?? false,
19446
+ cursorRepulsion,
19447
+ springyDrag: cfg?.springyDrag ?? false
19448
+ };
19449
+ }
19450
+
18502
19451
  // src/graphs/compile-graph.ts
18503
19452
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
18504
19453
  var SWATCH_SIZE2 = 12;
18505
19454
  var SWATCH_GAP2 = 6;
18506
19455
  var ENTRY_GAP2 = 16;
18507
- function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
19456
+ var ENERGY_PRESETS = {
19457
+ gentle: { chargeStrength: -150, velocityDecay: 0.5 },
19458
+ balanced: { chargeStrength: -300, velocityDecay: 0.4 },
19459
+ energetic: { chargeStrength: -600, velocityDecay: 0.3 }
19460
+ };
19461
+ var SETTLE_PRESETS = {
19462
+ quick: 0.05,
19463
+ balanced: 0.0228,
19464
+ thorough: 0.01
19465
+ };
19466
+ var DEFAULT_WARMUP_TICKS = 100;
19467
+ var DEFAULT_WARMUP_BUDGET_MS = 250;
19468
+ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField, legendOrder) {
18508
19469
  const labelStyle = {
18509
19470
  fontFamily: theme.fonts.family,
18510
19471
  fontSize: theme.fonts.sizes.small,
@@ -18514,25 +19475,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
18514
19475
  };
18515
19476
  let entries;
18516
19477
  if (hasCommunities && communityColorMap.size > 0) {
19478
+ const counts = /* @__PURE__ */ new Map();
19479
+ for (const node of nodes) {
19480
+ if (node.community != null) {
19481
+ counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
19482
+ }
19483
+ }
18517
19484
  entries = [...communityColorMap.entries()].map(([label, color2]) => ({
18518
19485
  label,
18519
19486
  color: color2,
18520
19487
  shape: "circle",
18521
- active: true
19488
+ active: true,
19489
+ count: counts.get(label) ?? 0
18522
19490
  }));
18523
19491
  } else if (nodeColorField) {
18524
19492
  const categoryColors = /* @__PURE__ */ new Map();
19493
+ const categoryCounts = /* @__PURE__ */ new Map();
18525
19494
  for (const node of nodes) {
18526
19495
  const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
18527
19496
  if (!categoryColors.has(category)) {
18528
19497
  categoryColors.set(category, node.fill);
18529
19498
  }
19499
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
18530
19500
  }
18531
- entries = categoryColors.size <= 1 ? [] : [...categoryColors.entries()].map(([label, color2]) => ({
19501
+ entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
18532
19502
  label,
18533
- color: color2,
19503
+ color: categoryColors.get(label),
18534
19504
  shape: "circle",
18535
- active: true
19505
+ active: true,
19506
+ count: categoryCounts.get(label) ?? 0
18536
19507
  }));
18537
19508
  } else {
18538
19509
  entries = [];
@@ -18548,6 +19519,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
18548
19519
  swatchChipFill: theme.colors.annotationFill
18549
19520
  };
18550
19521
  }
19522
+ function buildEdgeLegend(edges, encoding) {
19523
+ const field = encoding.edgeColor?.field;
19524
+ if (!field || (encoding.edgeColor?.type ?? "nominal") === "quantitative") return void 0;
19525
+ const categoryColors = /* @__PURE__ */ new Map();
19526
+ const categoryCounts = /* @__PURE__ */ new Map();
19527
+ for (const edge of edges) {
19528
+ const category = String(edge.data[field] ?? "");
19529
+ if (!categoryColors.has(category)) categoryColors.set(category, edge.stroke);
19530
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19531
+ }
19532
+ if (categoryColors.size <= 1) return void 0;
19533
+ const order = resolveCategoricalDomain(
19534
+ edges.map((e) => String(e.data[field] ?? "")),
19535
+ encoding.edgeColor?.sort,
19536
+ encoding.edgeColor?.scale?.domain
19537
+ );
19538
+ return order.filter((label) => categoryColors.has(label)).map((label) => ({
19539
+ label,
19540
+ color: categoryColors.get(label),
19541
+ shape: "line",
19542
+ active: true,
19543
+ count: categoryCounts.get(label) ?? 0
19544
+ }));
19545
+ }
18551
19546
  function buildGraphTooltips(nodes) {
18552
19547
  const descriptors = /* @__PURE__ */ new Map();
18553
19548
  for (const node of nodes) {
@@ -18574,6 +19569,21 @@ function buildGraphTooltips(nodes) {
18574
19569
  }
18575
19570
  return descriptors;
18576
19571
  }
19572
+ function buildEdgeTooltip(edge) {
19573
+ const fields = [];
19574
+ for (const [key, value2] of Object.entries(edge.data)) {
19575
+ if (key === "source" || key === "target") continue;
19576
+ if (value2 == null) continue;
19577
+ fields.push({
19578
+ label: key,
19579
+ value: typeof value2 === "number" ? graphNumberFormatter(value2) : String(value2)
19580
+ });
19581
+ }
19582
+ return {
19583
+ title: `${edge.source} \u2192 ${edge.target}`,
19584
+ fields
19585
+ };
19586
+ }
18577
19587
  function compileGraph(spec, options) {
18578
19588
  const { spec: normalized, warnings } = compile(spec);
18579
19589
  emitSpecWarnings(warnings, options.onWarn);
@@ -18583,6 +19593,16 @@ function compileGraph(spec, options) {
18583
19593
  );
18584
19594
  }
18585
19595
  const graphSpec = normalized;
19596
+ for (const [name, ch] of [
19597
+ ["nodeColor", graphSpec.encoding.nodeColor],
19598
+ ["edgeColor", graphSpec.encoding.edgeColor]
19599
+ ]) {
19600
+ if (ch?.sort != null && ch.type === "quantitative") {
19601
+ options.onWarn?.(
19602
+ `encoding.${name}.sort is ignored for a quantitative field (sort only orders categorical domains).`
19603
+ );
19604
+ }
19605
+ }
18586
19606
  const rawWatermark = spec.watermark;
18587
19607
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
18588
19608
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
@@ -18608,13 +19628,30 @@ function compileGraph(spec, options) {
18608
19628
  }
18609
19629
  const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
18610
19630
  const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
19631
+ const nodeColorField = graphSpec.encoding.nodeColor?.field;
19632
+ const legendOrder = nodeColorField ? resolveCategoricalDomain(
19633
+ compiledNodes.map((n) => String(n.data[nodeColorField] ?? n.label ?? n.id)),
19634
+ graphSpec.encoding.nodeColor?.sort,
19635
+ graphSpec.encoding.nodeColor?.scale?.domain
19636
+ ) : void 0;
18611
19637
  const legend = buildGraphLegend(
18612
19638
  compiledNodes,
18613
19639
  communityColorMap,
18614
19640
  useCommunitiesForLegend,
18615
19641
  theme,
18616
- graphSpec.encoding.nodeColor?.field
19642
+ nodeColorField,
19643
+ legendOrder
18617
19644
  );
19645
+ const edgeLegend = buildEdgeLegend(compiledEdges, graphSpec.encoding);
19646
+ const legendField = nodeColorField ?? (useCommunitiesForLegend ? clusteringField ?? null : null);
19647
+ let initialHighlight;
19648
+ if (nodeColorField && graphSpec.encoding.nodeColor?.highlight != null) {
19649
+ const raw = graphSpec.encoding.nodeColor.highlight;
19650
+ const requested = Array.isArray(raw) ? raw : [raw];
19651
+ const available = new Set(legendOrder ?? []);
19652
+ const values = requested.filter((v) => available.has(v));
19653
+ if (values.length > 0) initialHighlight = { field: nodeColorField, values };
19654
+ }
18618
19655
  const tooltipDescriptors = buildGraphTooltips(compiledNodes);
18619
19656
  const communityCount = communityColorMap.size;
18620
19657
  const altParts = [
@@ -18631,17 +19668,26 @@ function compileGraph(spec, options) {
18631
19668
  };
18632
19669
  const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
18633
19670
  const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
19671
+ const energyPreset = graphSpec.layout.energy ? ENERGY_PRESETS[graphSpec.layout.energy] : ENERGY_PRESETS.balanced;
19672
+ const settlePreset = graphSpec.layout.settle ? SETTLE_PRESETS[graphSpec.layout.settle] : SETTLE_PRESETS.balanced;
19673
+ const warmupRaw = graphSpec.layout.warmup;
19674
+ const warmupTicks = warmupRaw === false ? 0 : typeof warmupRaw === "number" ? Math.max(0, Math.floor(warmupRaw)) : DEFAULT_WARMUP_TICKS;
18634
19675
  const simulationConfig = {
18635
- chargeStrength: graphSpec.layout.chargeStrength ?? -300,
19676
+ chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
18636
19677
  linkDistance: graphSpec.layout.linkDistance ?? 30,
18637
19678
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
18638
- alphaDecay: 0.0228,
18639
- velocityDecay: 0.4,
19679
+ alphaDecay: settlePreset,
19680
+ velocityDecay: energyPreset.velocityDecay,
18640
19681
  collisionRadius: maxRadius + collisionPadding,
18641
19682
  collisionPadding,
18642
19683
  linkStrength: graphSpec.layout.linkStrength,
18643
- centerForce: graphSpec.layout.centerForce
19684
+ centerForce: graphSpec.layout.centerForce,
19685
+ seed: graphSpec.layout.seed,
19686
+ warmupTicks,
19687
+ warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
18644
19688
  };
19689
+ const animation = resolveGraphAnimation(graphSpec.animation);
19690
+ const interaction = resolveGraphInteraction(graphSpec.interaction);
18645
19691
  const chrome = computeChrome4(
18646
19692
  {
18647
19693
  title: graphSpec.chrome.title,
@@ -18670,7 +19716,12 @@ function compileGraph(spec, options) {
18670
19716
  height: options.height
18671
19717
  },
18672
19718
  simulationConfig,
18673
- watermark
19719
+ watermark,
19720
+ animation,
19721
+ interaction,
19722
+ legendField,
19723
+ initialHighlight,
19724
+ edgeLegend
18674
19725
  };
18675
19726
  }
18676
19727
  var DEFAULT_COLLISION_PADDING = 5;
@@ -18763,6 +19814,7 @@ function legendLayoutHasContent(legendLayout) {
18763
19814
  }
18764
19815
  var MIN_CHART_WIDTH = 60;
18765
19816
  var MIN_CHART_HEIGHT2 = 40;
19817
+ var PLOT_SHARE_MAX = 0.4;
18766
19818
  var SERIES_SEARCH_INPUT_HEIGHT = 32;
18767
19819
  var SERIES_SEARCH_GAP = 12;
18768
19820
  function getMinChartDims(display) {
@@ -18805,8 +19857,10 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
18805
19857
  watermark,
18806
19858
  bottomLegendReservation
18807
19859
  );
19860
+ const chromeLayout = resolveChromeLayout(spec, options);
19861
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
18808
19862
  if (isSparkline) {
18809
- const total2 = { x: 0, y: 0, width, height };
19863
+ const total2 = { x: 0, y: 0, width, height: effectiveHeight };
18810
19864
  const sparkPad = getSparklinePad(spec);
18811
19865
  const xAxisSpace = userExplicit.xAxis ? 26 : 0;
18812
19866
  const yAxisSpace = userExplicit.yAxis ? 30 : 0;
@@ -18830,7 +19884,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
18830
19884
  x: margins2.left,
18831
19885
  y: margins2.top,
18832
19886
  width: Math.max(0, width - margins2.left - margins2.right),
18833
- height: Math.max(0, height - margins2.top - margins2.bottom)
19887
+ height: Math.max(0, effectiveHeight - margins2.top - margins2.bottom)
18834
19888
  };
18835
19889
  return {
18836
19890
  total: total2,
@@ -18842,7 +19896,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
18842
19896
  effectiveAxisGap: 0
18843
19897
  };
18844
19898
  }
18845
- const total = { x: 0, y: 0, width, height };
19899
+ const total = { x: 0, y: 0, width, height: effectiveHeight };
18846
19900
  const isRadial = isAxislessMark2(spec.markType);
18847
19901
  const encoding = spec.encoding;
18848
19902
  const xAxisSuppressed = encoding.x?.axis === false;
@@ -19072,10 +20126,11 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19072
20126
  x: margins.left,
19073
20127
  y: margins.top,
19074
20128
  width: Math.max(0, width - margins.left - margins.right),
19075
- height: Math.max(0, height - margins.top - margins.bottom)
20129
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19076
20130
  };
19077
20131
  const minDims = getMinChartDims(spec.display);
19078
- if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
20132
+ const chromeEatsPlot = chromeLayout !== "grow" && chrome.topHeight > height * PLOT_SHARE_MAX;
20133
+ if ((chartArea.width < minDims.width || chartArea.height < minDims.height || chromeEatsPlot) && chromeMode !== "hidden") {
19079
20134
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
19080
20135
  const fallbackChrome = computeChrome5(
19081
20136
  chromeToInput(spec.chrome),
@@ -19101,7 +20156,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19101
20156
  x: margins.left,
19102
20157
  y: margins.top,
19103
20158
  width: Math.max(0, width - margins.left - margins.right),
19104
- height: Math.max(0, height - margins.top - margins.bottom)
20159
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19105
20160
  };
19106
20161
  const fallbackMetricsTopY = topPad + fallbackChrome.topHeight;
19107
20162
  const fallbackMetricsArea = { x: hPad, width: Math.max(0, width - hPad * 2) };
@@ -19118,7 +20173,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19118
20173
  chartArea = {
19119
20174
  ...chartArea,
19120
20175
  y: margins.top,
19121
- height: Math.max(0, height - margins.top - margins.bottom)
20176
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19122
20177
  };
19123
20178
  }
19124
20179
  return {
@@ -19134,7 +20189,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19134
20189
  fallbackMetricsArea
19135
20190
  ) : void 0,
19136
20191
  xAxisHeight,
19137
- effectiveAxisGap: fallbackEffectiveAxisGap
20192
+ effectiveAxisGap: fallbackEffectiveAxisGap,
20193
+ chromeStripped: true
19138
20194
  };
19139
20195
  }
19140
20196
  }
@@ -19146,7 +20202,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19146
20202
  chartArea = {
19147
20203
  ...chartArea,
19148
20204
  y: margins.top,
19149
- height: Math.max(0, height - margins.top - margins.bottom)
20205
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19150
20206
  };
19151
20207
  }
19152
20208
  return {
@@ -19898,14 +20954,19 @@ function compileSankey(spec, options) {
19898
20954
  watermark
19899
20955
  );
19900
20956
  const padding = theme.spacing.padding;
20957
+ const chromeLayout = resolveChromeLayout(
20958
+ spec,
20959
+ options
20960
+ );
20961
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
19901
20962
  const fullArea = {
19902
20963
  x: padding,
19903
20964
  y: padding + chrome.topHeight,
19904
20965
  width: options.width - padding * 2,
19905
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
20966
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
19906
20967
  };
19907
20968
  if (fullArea.width <= 0 || fullArea.height <= 0) {
19908
- return emptyLayout4(fullArea, chrome, theme, options, watermark);
20969
+ return emptyLayout4(fullArea, chrome, theme, { ...options, height: grownHeight }, watermark);
19909
20970
  }
19910
20971
  const sourceField = sankeySpec.encoding.source.field;
19911
20972
  const targetField = sankeySpec.encoding.target.field;
@@ -19945,7 +21006,7 @@ function compileSankey(spec, options) {
19945
21006
  height: fullArea.height - legend.bounds.height - legendGap2
19946
21007
  };
19947
21008
  if (area.height <= 0) {
19948
- return emptyLayout4(area, chrome, theme, options, watermark);
21009
+ return emptyLayout4(area, chrome, theme, { ...options, height: grownHeight }, watermark);
19949
21010
  }
19950
21011
  const labelFontSize = theme.fonts.sizes.small;
19951
21012
  const labelFontWeight = theme.fonts.weights.normal;
@@ -20097,7 +21158,7 @@ function compileSankey(spec, options) {
20097
21158
  theme,
20098
21159
  dimensions: {
20099
21160
  width: options.width,
20100
- height: options.height
21161
+ height: grownHeight
20101
21162
  },
20102
21163
  animation: resolvedAnimation,
20103
21164
  watermark,
@@ -20906,7 +21967,7 @@ import {
20906
21967
  HPAD_COMPACT_MIN as HPAD_COMPACT_MIN4,
20907
21968
  pickLabelColor as pickLabelColor4,
20908
21969
  resolveTheme as resolveTheme6,
20909
- SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
21970
+ SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES3
20910
21971
  } from "@opendata-ai/openchart-core";
20911
21972
  var TILE_CORNER_RADIUS = 6;
20912
21973
  var TILE_STROKE_WIDTH = 1;
@@ -20916,6 +21977,10 @@ var LEGEND_ENTRY_GAP = 16;
20916
21977
  function clamp(value2, min4, max4) {
20917
21978
  return Math.min(max4, Math.max(min4, value2));
20918
21979
  }
21980
+ function growContentHeight(contentHeight, chromeLayout, budgetHeight, chrome) {
21981
+ if (chromeLayout !== "grow") return contentHeight;
21982
+ return Math.max(contentHeight, budgetHeight + chrome.topHeight + chrome.bottomHeight);
21983
+ }
20919
21984
  function compileTileMap(spec, options) {
20920
21985
  const sugarWarnings = [];
20921
21986
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
@@ -20952,22 +22017,44 @@ function compileTileMap(spec, options) {
20952
22017
  watermark
20953
22018
  );
20954
22019
  const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
22020
+ const chromeLayout = resolveChromeLayout(
22021
+ spec,
22022
+ options
22023
+ );
20955
22024
  const fullArea = {
20956
22025
  x: padding,
20957
22026
  y: padding + chrome.topHeight,
20958
22027
  width: options.width - padding * 2,
20959
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
22028
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
20960
22029
  };
20961
22030
  if (fullArea.width <= 0 || fullArea.height <= 0) {
20962
22031
  return emptyLayout5(chrome, theme, options, watermark);
20963
22032
  }
20964
22033
  const isCategorical = tilemapSpec.encoding.color !== void 0 || tilemapSpec.colors !== void 0;
20965
22034
  if (isCategorical) {
20966
- return compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
22035
+ return compileCategorical(
22036
+ tilemapSpec,
22037
+ options,
22038
+ theme,
22039
+ chrome,
22040
+ fullArea,
22041
+ isDarkMode,
22042
+ watermark,
22043
+ chromeLayout
22044
+ );
20967
22045
  }
20968
- return compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
22046
+ return compileQuantitative(
22047
+ tilemapSpec,
22048
+ options,
22049
+ theme,
22050
+ chrome,
22051
+ fullArea,
22052
+ isDarkMode,
22053
+ watermark,
22054
+ chromeLayout
22055
+ );
20969
22056
  }
20970
- function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22057
+ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
20971
22058
  const stateField = tilemapSpec.encoding.state.field;
20972
22059
  const valueField = tilemapSpec.encoding.value.field;
20973
22060
  const stateValueMap = /* @__PURE__ */ new Map();
@@ -20984,7 +22071,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
20984
22071
  const values = Array.from(stateValueMap.values());
20985
22072
  const min4 = values.length > 0 ? Math.min(...values) : 0;
20986
22073
  const max4 = values.length > 0 ? Math.max(...values) : 100;
20987
- const paletteStops = [...SEQUENTIAL_PALETTES2[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES2.blue];
22074
+ const paletteStops = [...SEQUENTIAL_PALETTES3[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES3.blue];
20988
22075
  const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
20989
22076
  const opacityRange = isDarkMode ? [0.15, 1] : [0.2, 1];
20990
22077
  const opacityScale = linear2().domain([min4, max4]).range(opacityRange).clamp(true);
@@ -21068,7 +22155,12 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21068
22155
  };
21069
22156
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21070
22157
  const padding = fullArea.x;
21071
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22158
+ const contentHeight = growContentHeight(
22159
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22160
+ chromeLayout,
22161
+ options.height,
22162
+ chrome
22163
+ );
21072
22164
  return {
21073
22165
  area: fullArea,
21074
22166
  chrome,
@@ -21085,7 +22177,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21085
22177
  measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth23(text, fontSize), height: fontSize }))
21086
22178
  };
21087
22179
  }
21088
- function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22180
+ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21089
22181
  const stateField = tilemapSpec.encoding.state.field;
21090
22182
  const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
21091
22183
  const stateCategoryMap = /* @__PURE__ */ new Map();
@@ -21204,7 +22296,12 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
21204
22296
  };
21205
22297
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21206
22298
  const padding = fullArea.x;
21207
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22299
+ const contentHeight = growContentHeight(
22300
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22301
+ chromeLayout,
22302
+ options.height,
22303
+ chrome
22304
+ );
21208
22305
  return {
21209
22306
  area: fullArea,
21210
22307
  chrome,
@@ -22264,6 +23361,38 @@ function applySparklineDefaults(spec, theme) {
22264
23361
  }
22265
23362
  return { ...spec, markDef: newMarkDef };
22266
23363
  }
23364
+ function resolveScaleInvert(resolved) {
23365
+ if (!resolved) return void 0;
23366
+ const { scale, type } = resolved;
23367
+ if (type === "band" || type === "point") {
23368
+ const domain = scale.domain();
23369
+ const positions = domain.map((v) => {
23370
+ const pos = scale(v) ?? 0;
23371
+ return type === "band" ? pos + scale.bandwidth() / 2 : pos;
23372
+ });
23373
+ const range4 = scale.range();
23374
+ return {
23375
+ topPixel: range4[0],
23376
+ bottomPixel: range4[range4.length - 1],
23377
+ topData: 0,
23378
+ bottomData: 0,
23379
+ domain,
23380
+ positions
23381
+ };
23382
+ }
23383
+ const invert = scale.invert;
23384
+ if (typeof invert !== "function") return void 0;
23385
+ const range3 = scale.range();
23386
+ const topPx = range3[0];
23387
+ const bottomPx = range3[range3.length - 1];
23388
+ const topVal = invert.call(scale, topPx);
23389
+ const bottomVal = invert.call(scale, bottomPx);
23390
+ const topData = topVal instanceof Date ? +topVal : Number(topVal);
23391
+ const bottomData = bottomVal instanceof Date ? +bottomVal : Number(bottomVal);
23392
+ if (!Number.isFinite(topData) || !Number.isFinite(bottomData)) return void 0;
23393
+ const scaleType = type;
23394
+ return { topPixel: topPx, bottomPixel: bottomPx, topData, bottomData, scaleType };
23395
+ }
22267
23396
  function compileChart(spec, optionsInput) {
22268
23397
  const sugarWarnings = [];
22269
23398
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
@@ -22464,12 +23593,35 @@ function compileChart(spec, optionsInput) {
22464
23593
  }
22465
23594
  };
22466
23595
  }
23596
+ let facetDirection = "wrap";
23597
+ const rowChannel = renderSpec.encoding.row;
23598
+ const colChannel = renderSpec.encoding.column;
23599
+ if (rowChannel?.field || colChannel?.field) {
23600
+ const channel = rowChannel?.field ? rowChannel : colChannel;
23601
+ facetDirection = rowChannel?.field ? "row" : "column";
23602
+ const desugared = { ...renderSpec.encoding };
23603
+ delete desugared.row;
23604
+ delete desugared.column;
23605
+ desugared.facet = {
23606
+ field: channel.field,
23607
+ type: channel.type,
23608
+ sort: channel.sort,
23609
+ ...facetDirection === "row" ? { columns: 1 } : {}
23610
+ };
23611
+ renderSpec = { ...renderSpec, encoding: desugared };
23612
+ const chartDesugared = { ...chartSpec.encoding };
23613
+ delete chartDesugared.row;
23614
+ delete chartDesugared.column;
23615
+ chartDesugared.facet = desugared.facet;
23616
+ chartSpec = { ...chartSpec, encoding: chartDesugared };
23617
+ }
22467
23618
  const facetChannel = renderSpec.encoding.facet;
22468
23619
  if (facetChannel?.field) {
22469
23620
  return compileFaceted(
22470
23621
  chartSpec,
22471
23622
  renderSpec,
22472
23623
  facetChannel,
23624
+ facetDirection,
22473
23625
  options,
22474
23626
  theme,
22475
23627
  strategy,
@@ -22534,7 +23686,10 @@ function compileChart(spec, optionsInput) {
22534
23686
  skipY,
22535
23687
  markType: chartSpec.markType,
22536
23688
  totalWidth: options.width,
22537
- precomputedYTicks: plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23689
+ // If the guardrail stripped chrome, the plan's y-ticks were sized
23690
+ // against the pre-strip (shorter) plot. Drop them so computeAxes
23691
+ // regenerates against the final (taller) chartArea.
23692
+ precomputedYTicks: !dims.chromeStripped && plan.yTickValues.length > 0 ? plan.yTickValues : void 0
22538
23693
  });
22539
23694
  const xAxisExtent = axes.x?.extent ?? 0;
22540
23695
  dims.chrome.bottomAnchorY = chartArea.y + chartArea.height + xAxisExtent;
@@ -22549,6 +23704,19 @@ function compileChart(spec, optionsInput) {
22549
23704
  const renderer = getChartRenderer(rendererKey);
22550
23705
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
22551
23706
  applyFillPatterns(marks, chartSpec.markDef, theme);
23707
+ const markRenderModeWarnings = [];
23708
+ const markRenderMode = resolveMarkRenderMode(
23709
+ {
23710
+ markDef: chartSpec.markDef,
23711
+ markType: chartSpec.markType,
23712
+ pointCount: marks.reduce((n, m) => m.type === "point" ? n + 1 : n, 0),
23713
+ display: chartSpec.display,
23714
+ faceted: false,
23715
+ layered: options.layered ?? false
23716
+ },
23717
+ markRenderModeWarnings
23718
+ );
23719
+ emitSpecWarnings(markRenderModeWarnings, options.onWarn);
22552
23720
  const xFieldForYdi = chartSpec.encoding.x && "field" in chartSpec.encoding.x ? chartSpec.encoding.x.field : void 0;
22553
23721
  const youDrawIt = resolveYouDrawIt(chartSpec.youDrawIt, marks, scales, chartArea, xFieldForYdi);
22554
23722
  const endpointLabels = computeEndpointLabels(
@@ -22629,6 +23797,8 @@ function compileChart(spec, optionsInput) {
22629
23797
  );
22630
23798
  assignAnimationIndices(marks, resolvedAnimation);
22631
23799
  const chrome = footnotes ? { ...dims.chrome, footnotes } : dims.chrome;
23800
+ const xInvert = resolveScaleInvert(scales.x);
23801
+ const yInvert = resolveScaleInvert(scales.y);
22632
23802
  return {
22633
23803
  area: chartArea,
22634
23804
  chrome,
@@ -22648,6 +23818,7 @@ function compileChart(spec, optionsInput) {
22648
23818
  a11y: {
22649
23819
  altText,
22650
23820
  dataTableFallback,
23821
+ ...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
22651
23822
  role: "img",
22652
23823
  keyboardNavigable: marks.length > 0,
22653
23824
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
@@ -22655,16 +23826,22 @@ function compileChart(spec, optionsInput) {
22655
23826
  theme,
22656
23827
  dimensions: {
22657
23828
  width: options.width,
22658
- height: options.height
23829
+ // In chromeLayout 'grow' mode, computeDimensions grows total.height by the
23830
+ // chrome height so the plot keeps its full budget. In the default
23831
+ // 'subtract' mode total.height === options.height, so this is a no-op.
23832
+ height: dims.total.height
22659
23833
  },
22660
23834
  animation: resolvedAnimation,
22661
23835
  watermark,
22662
23836
  display: chartSpec.display,
22663
23837
  crosshair,
22664
- measureText: options.measureText
23838
+ measureText: options.measureText,
23839
+ ...xInvert ? { xInvert } : {},
23840
+ ...yInvert ? { yInvert } : {},
23841
+ ...markRenderMode === "canvas" ? { markRenderMode } : {}
22665
23842
  };
22666
23843
  }
22667
- function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
23844
+ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
22668
23845
  const measure = createMeasureFn(options.measureText);
22669
23846
  const panelEncoding = { ...renderSpec.encoding };
22670
23847
  delete panelEncoding.facet;
@@ -22710,10 +23887,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
22710
23887
  plan.xAxisExtent,
22711
23888
  dims.effectiveAxisGap
22712
23889
  );
22713
- const leftReservation = plan.leftGutter;
22714
23890
  const bottomReservation = plan.xAxisExtent;
22715
- const grid = computeFacetGrid(facetValues, facetChannel.columns, chartArea, {
22716
- left: leftReservation,
23891
+ const effectiveColumns = facetDirection === "column" ? facetValues.length : facetChannel.columns;
23892
+ const grid = computeFacetGrid(facetValues, effectiveColumns, chartArea, {
23893
+ left: 0,
22717
23894
  bottom: bottomReservation
22718
23895
  });
22719
23896
  if (!optionsInput.facetHeightGrown && grid.panels.length > 0 && grid.panels[0].area.height < MIN_PANEL_HEIGHT) {
@@ -22728,8 +23905,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
22728
23905
  }
22729
23906
  }
22730
23907
  const resolveConfig = chartSpec.resolve;
22731
- const yResolve = resolveConfig?.scale?.y ?? "shared";
22732
- const xResolve = resolveConfig?.scale?.x ?? "shared";
23908
+ const xDefault = facetDirection === "column" ? "independent" : "shared";
23909
+ const yDefault = facetDirection === "row" ? "independent" : "shared";
23910
+ const yResolve = resolveConfig?.scale?.y ?? yDefault;
23911
+ const xResolve = resolveConfig?.scale?.x ?? xDefault;
22733
23912
  const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
22734
23913
  const sharedXDomain = xResolve === "shared" && fullScales.x ? fullScales.x.scale.domain() : void 0;
22735
23914
  const sharedYDomain = yResolve === "shared" && fullScales.y ? fullScales.y.scale.domain() : void 0;
@@ -22794,6 +23973,16 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
22794
23973
  tickMarks: false
22795
23974
  };
22796
23975
  }
23976
+ const isBottomRow = gridPanel.row === grid.rows - 1;
23977
+ if (xResolve === "shared" && facetDirection === "row" && !isBottomRow && panelAxes.x) {
23978
+ panelAxes.x = {
23979
+ ...panelAxes.x,
23980
+ ticks: [],
23981
+ label: void 0,
23982
+ domainLine: false,
23983
+ tickMarks: false
23984
+ };
23985
+ }
22797
23986
  const panelMarks = renderer ? renderer(
22798
23987
  panelSpecWithDomains,
22799
23988
  panelScales,
@@ -22847,10 +24036,11 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
22847
24036
  annotations: panelAnnotations,
22848
24037
  header: {
22849
24038
  text: gridPanel.key,
22850
- x: gridPanel.headerPos.x,
24039
+ x: facetDirection === "row" ? gridPanel.area.x : gridPanel.headerPos.x,
22851
24040
  y: gridPanel.headerPos.y,
22852
24041
  fontSize: theme.fonts.sizes.body,
22853
- fontWeight: theme.fonts.weights.medium
24042
+ fontWeight: theme.fonts.weights.medium,
24043
+ ...facetDirection === "row" ? { textAnchor: "start" } : {}
22854
24044
  }
22855
24045
  });
22856
24046
  }
@@ -22903,6 +24093,7 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
22903
24093
  a11y: {
22904
24094
  altText,
22905
24095
  dataTableFallback,
24096
+ ...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
22906
24097
  role: "img",
22907
24098
  keyboardNavigable: allMarks.length > 0,
22908
24099
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
@@ -22918,7 +24109,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
22918
24109
  panels: panelLayouts,
22919
24110
  facetField: facetChannel.field,
22920
24111
  columns: grid.columns,
22921
- sharedScales: yResolve === "shared" && xResolve === "shared"
24112
+ sharedScales: yResolve === "shared" && xResolve === "shared",
24113
+ direction: facetDirection
22922
24114
  }
22923
24115
  };
22924
24116
  }
@@ -22972,6 +24164,7 @@ export {
22972
24164
  EXIT_DEFAULTS,
22973
24165
  UPDATE_DEFAULTS,
22974
24166
  buildAreaPath,
24167
+ buildEdgeTooltip,
22975
24168
  buildLinePath,
22976
24169
  clampStaggerDelay,
22977
24170
  clearRenderers,