@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/index.d.ts +175 -12
  2. package/dist/index.js +917 -168
  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 -2401
  7. package/src/__tests__/annotation-behavior.test.ts +0 -852
  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 -376
  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 -545
  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 -630
  133. package/src/compile/size-scale.ts +0 -141
  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 -1503
  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 -170
  165. package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
  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 -734
  174. package/src/compiler/sparkline-defaults.ts +0 -138
  175. package/src/compiler/types.ts +0 -241
  176. package/src/compiler/validate.ts +0 -2041
  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 -1128
  185. package/src/geo/compile-map.ts +0 -1376
  186. package/src/geo/join.ts +0 -115
  187. package/src/geo/projections.ts +0 -65
  188. package/src/geo/types.ts +0 -26
  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 -748
  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
 
@@ -8559,6 +8560,11 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
8559
8560
  width: textWidth,
8560
8561
  height: textHeight,
8561
8562
  priority: "data",
8563
+ // Index into the ORIGINAL marks array, not `targetMarks`. Density
8564
+ // filtering drops slices and resolveCollisions re-sorts by priority, so
8565
+ // the returned labels line up with neither. Callers assign by this index
8566
+ // instead of zipping positionally.
8567
+ index: marks.indexOf(mark),
8562
8568
  style: {
8563
8569
  fontFamily: "system-ui, -apple-system, sans-serif",
8564
8570
  fontSize: LABEL_FONT_SIZE5,
@@ -8580,14 +8586,16 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
8580
8586
  x: c.anchorX,
8581
8587
  y: c.anchorY,
8582
8588
  style: c.style,
8583
- visible: true
8589
+ visible: true,
8590
+ ...c.index !== void 0 ? { index: c.index } : {}
8584
8591
  }));
8585
8592
  } else {
8586
8593
  resolved = resolveCollisions5(candidates);
8587
8594
  }
8588
- for (let i = 0; i < resolved.length && i < targetMarks.length; i++) {
8595
+ for (let i = 0; i < resolved.length; i++) {
8589
8596
  const label = resolved[i];
8590
- const mark = targetMarks[i];
8597
+ const mark = label.index !== void 0 ? marks[label.index] : void 0;
8598
+ if (!mark) continue;
8591
8599
  if (label.visible) {
8592
8600
  label.connector = {
8593
8601
  from: { x: label.x, y: label.y },
@@ -8605,16 +8613,16 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
8605
8613
  var pieRenderer = (spec, scales, chartArea, strategy, theme) => {
8606
8614
  const marks = computePieMarks(spec, scales, chartArea, strategy, false);
8607
8615
  const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
8608
- for (let i = 0; i < marks.length && i < labels.length; i++) {
8609
- marks[i].label = labels[i];
8616
+ for (const label of labels) {
8617
+ if (label.index !== void 0 && marks[label.index]) marks[label.index].label = label;
8610
8618
  }
8611
8619
  return marks;
8612
8620
  };
8613
8621
  var donutRenderer = (spec, scales, chartArea, strategy, theme) => {
8614
8622
  const marks = computePieMarks(spec, scales, chartArea, strategy, true);
8615
8623
  const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
8616
- for (let i = 0; i < marks.length && i < labels.length; i++) {
8617
- marks[i].label = labels[i];
8624
+ for (const label of labels) {
8625
+ if (label.index !== void 0 && marks[label.index]) marks[label.index].label = label;
8618
8626
  }
8619
8627
  return marks;
8620
8628
  };
@@ -9062,6 +9070,7 @@ var ruleRenderer = (spec, scales, chartArea, _strategy, _theme) => {
9062
9070
  };
9063
9071
 
9064
9072
  // src/charts/scatter/compute.ts
9073
+ import { isOpaqueColor } from "@opendata-ai/openchart-core";
9065
9074
  var DEFAULT_POINT_RADIUS2 = 5;
9066
9075
  function resolvePosition2(value2, channelType, scale) {
9067
9076
  switch (channelType) {
@@ -9087,7 +9096,7 @@ function resolvePosition2(value2, channelType, scale) {
9087
9096
  }
9088
9097
  }
9089
9098
  }
9090
- function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9099
+ function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
9091
9100
  const encoding = spec.encoding;
9092
9101
  const xChannel = encoding.x;
9093
9102
  const yChannel = encoding.y;
@@ -9105,6 +9114,10 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9105
9114
  const sizeField = sizeEnc?.field;
9106
9115
  const resolvedSize = buildSizeScale(sizeEnc, spec.data, SIZE_SCALE_DEFAULTS.point);
9107
9116
  const sizeScale = resolvedSize?.scale;
9117
+ const constFill = spec.markDef.fill;
9118
+ const constSize = spec.markDef.size;
9119
+ const bg = theme?.colors?.background;
9120
+ const defaultStroke = bg && isOpaqueColor(bg) ? bg : "#ffffff";
9108
9121
  const keyEnc = encoding.key && "field" in encoding.key ? encoding.key : void 0;
9109
9122
  const keyField = keyEnc?.field;
9110
9123
  const marks = [];
@@ -9119,10 +9132,12 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9119
9132
  if (isSequentialColor && colorField) {
9120
9133
  const val = Number(row[colorField]);
9121
9134
  color2 = Number.isFinite(val) ? getSequentialColor(scales, val) : getColor(scales, "__default__");
9122
- } else {
9135
+ } else if (colorField) {
9123
9136
  color2 = getColor(scales, category ?? "__default__");
9137
+ } else {
9138
+ color2 = constFill ?? getColor(scales, "__default__");
9124
9139
  }
9125
- let radius = DEFAULT_POINT_RADIUS2;
9140
+ let radius = constSize != null ? constSize : DEFAULT_POINT_RADIUS2;
9126
9141
  if (sizeScale && sizeField) {
9127
9142
  const sizeVal = Number(row[sizeField]);
9128
9143
  if (Number.isFinite(sizeVal)) {
@@ -9143,9 +9158,9 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
9143
9158
  cy,
9144
9159
  r: radius,
9145
9160
  fill: color2,
9146
- stroke: "#ffffff",
9147
- strokeWidth: 1,
9148
- fillOpacity: 0.7,
9161
+ stroke: spec.markDef.stroke ?? defaultStroke,
9162
+ strokeWidth: spec.markDef.strokeWidth ?? 1,
9163
+ fillOpacity: spec.markDef.opacity ?? 0.7,
9149
9164
  data: row,
9150
9165
  aria
9151
9166
  });
@@ -9186,7 +9201,7 @@ function linearRegression(points) {
9186
9201
  const intercept = (sumY - slope * sumX) / n;
9187
9202
  return { slope, intercept };
9188
9203
  }
9189
- function computeTrendLine(marks) {
9204
+ function computeTrendLine(marks, stroke = TRENDLINE_COLOR, strokeWidth = TRENDLINE_STROKE_WIDTH) {
9190
9205
  if (marks.length < 2) return null;
9191
9206
  const points = marks.map((m) => ({ x: m.cx, y: m.cy }));
9192
9207
  const result = linearRegression(points);
@@ -9209,8 +9224,8 @@ function computeTrendLine(marks) {
9209
9224
  { x: minX, y: y12 },
9210
9225
  { x: maxX, y: y2 }
9211
9226
  ],
9212
- stroke: TRENDLINE_COLOR,
9213
- strokeWidth: TRENDLINE_STROKE_WIDTH,
9227
+ stroke,
9228
+ strokeWidth,
9214
9229
  strokeDasharray: TRENDLINE_DASH,
9215
9230
  data: [],
9216
9231
  aria
@@ -9218,13 +9233,20 @@ function computeTrendLine(marks) {
9218
9233
  }
9219
9234
 
9220
9235
  // src/charts/scatter/index.ts
9221
- var scatterRenderer = (spec, scales, chartArea, strategy, _theme) => {
9222
- const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy);
9236
+ var scatterRenderer = (spec, scales, chartArea, strategy, theme) => {
9237
+ const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy, theme);
9223
9238
  const marks = [...pointMarks];
9224
- if (spec.markDef?.trendline !== false) {
9225
- const trendLine = computeTrendLine(pointMarks);
9239
+ const trendline = spec.markDef?.trendline;
9240
+ if (trendline !== false) {
9241
+ const config = typeof trendline === "object" ? trendline : void 0;
9242
+ const stroke = config?.stroke ?? theme?.colors?.text;
9243
+ const trendLine = computeTrendLine(pointMarks, stroke, config?.strokeWidth);
9226
9244
  if (trendLine) {
9227
- marks.unshift(trendLine);
9245
+ if (config?.layer === "above") {
9246
+ marks.push(trendLine);
9247
+ } else {
9248
+ marks.unshift(trendLine);
9249
+ }
9228
9250
  }
9229
9251
  }
9230
9252
  return marks;
@@ -9704,6 +9726,16 @@ function stripIgnoredKeys(spec, warnings) {
9704
9726
  const { $schema: _schema, ...rest } = spec;
9705
9727
  return rest;
9706
9728
  }
9729
+ function stripRemovedMarkRender(spec, warnings) {
9730
+ const mark = spec.mark;
9731
+ if (!mark || typeof mark !== "object" || Array.isArray(mark)) return spec;
9732
+ if (!("render" in mark)) return spec;
9733
+ warnings.push(
9734
+ "[openchart] mark.render was removed in v8. This field is stripped for backward compatibility. Pass { renderer } to createChart() or the compile options instead."
9735
+ );
9736
+ const { render: _render, ...restMark } = mark;
9737
+ return { ...spec, mark: restMark };
9738
+ }
9707
9739
  function applyFixedSizeDefault(spec) {
9708
9740
  if (typeof spec.width === "number" && typeof spec.height === "number" && spec.responsive === void 0) {
9709
9741
  return { ...spec, responsive: false };
@@ -9846,7 +9878,16 @@ function expandChannelSugar(spec, warnings) {
9846
9878
  }
9847
9879
  return result;
9848
9880
  }
9849
- var COUNT_GROUP_CHANNELS = ["x", "y", "color", "detail", "strokeDash", "facet"];
9881
+ var COUNT_GROUP_CHANNELS = [
9882
+ "x",
9883
+ "y",
9884
+ "color",
9885
+ "detail",
9886
+ "strokeDash",
9887
+ "facet",
9888
+ "row",
9889
+ "column"
9890
+ ];
9850
9891
  function expandCountAggregate(spec) {
9851
9892
  const encoding = spec.encoding;
9852
9893
  if (!encoding || typeof encoding !== "object") return spec;
@@ -9965,6 +10006,7 @@ function expandChartSugar(spec, warnings, inheritedData) {
9965
10006
  out = expandTopLevelTitle(out);
9966
10007
  out = expandDescriptionSugar(out);
9967
10008
  out = stripIgnoredKeys(out, warnings);
10009
+ out = stripRemovedMarkRender(out, warnings);
9968
10010
  out = applyFixedSizeDefault(out);
9969
10011
  out = expandAnnotationSugar(out, warnings);
9970
10012
  out = expandChannelSugar(out, warnings);
@@ -10081,6 +10123,12 @@ function resolveAnimation(spec) {
10081
10123
  const enter = resolvePhase(config.enter, ENTER_DEFAULTS);
10082
10124
  const update = resolvePhase(config.update, UPDATE_DEFAULTS);
10083
10125
  const exit = resolvePhase(config.exit, EXIT_DEFAULTS);
10126
+ if (update && config.update && typeof config.update === "object" && !Array.isArray(config.update)) {
10127
+ const cap = config.update.maxMarks;
10128
+ if (typeof cap === "number" && Number.isFinite(cap) && cap >= 1) {
10129
+ update.maxMarks = Math.floor(cap);
10130
+ }
10131
+ }
10084
10132
  if (!enter && !update && !exit) return void 0;
10085
10133
  return {
10086
10134
  enter: enter ?? void 0,
@@ -10122,9 +10170,9 @@ function resolveStagger(stagger, defaults) {
10122
10170
  import {
10123
10171
  isBarListSpec,
10124
10172
  isChartSpec,
10173
+ isGeoMapSpec,
10125
10174
  isGraphSpec,
10126
10175
  isLayerSpec,
10127
- isMapSpec,
10128
10176
  isSankeySpec,
10129
10177
  isTableSpec,
10130
10178
  isTileMapSpec,
@@ -10473,6 +10521,7 @@ function normalizeChartSpec(spec, warnings) {
10473
10521
  data: spec.data,
10474
10522
  encoding,
10475
10523
  chrome: normalizeChrome(spec.chrome),
10524
+ chromeLayout: spec.chromeLayout,
10476
10525
  metrics: spec.metrics,
10477
10526
  seriesSearch: normalizeSeriesSearch(spec, encoding, warnings),
10478
10527
  youDrawIt: normalizeYouDrawIt(spec),
@@ -10546,16 +10595,16 @@ function normalizeSankeySpec(spec, _warnings) {
10546
10595
  watermark: spec.watermark ?? true
10547
10596
  };
10548
10597
  }
10549
- function normalizeGraphSpec(spec) {
10550
- const defaultLayout = {
10598
+ function normalizeGraphSpec(spec, warnings) {
10599
+ const layout = {
10551
10600
  type: "force",
10552
- chargeStrength: -300,
10553
- linkDistance: 30
10554
- };
10555
- const layout = spec.layout ? {
10556
- ...defaultLayout,
10557
10601
  ...spec.layout
10558
- } : defaultLayout;
10602
+ };
10603
+ let seedNode = typeof spec.seedNode === "string" ? { id: spec.seedNode } : spec.seedNode;
10604
+ if (seedNode && !spec.nodes.some((n) => n.id === seedNode?.id)) {
10605
+ warnings.push(`seedNode "${seedNode.id}" does not match any node id; it will be ignored.`);
10606
+ seedNode = void 0;
10607
+ }
10559
10608
  return {
10560
10609
  type: "graph",
10561
10610
  nodes: spec.nodes,
@@ -10563,11 +10612,15 @@ function normalizeGraphSpec(spec) {
10563
10612
  encoding: spec.encoding ?? {},
10564
10613
  layout,
10565
10614
  nodeOverrides: spec.nodeOverrides,
10615
+ seedNode,
10566
10616
  chrome: normalizeChrome(spec.chrome),
10567
10617
  annotations: normalizeAnnotations(spec.annotations),
10568
10618
  theme: spec.theme ?? {},
10569
10619
  darkMode: spec.darkMode ?? "off",
10570
- watermark: spec.watermark ?? true
10620
+ watermark: spec.watermark ?? true,
10621
+ animation: spec.animation,
10622
+ interaction: spec.interaction,
10623
+ legend: spec.legend
10571
10624
  };
10572
10625
  }
10573
10626
  function normalizeTileMapSpec(spec, warnings) {
@@ -10623,7 +10676,7 @@ function normalizeTileMapSpec(spec, warnings) {
10623
10676
  valueFormat: spec.valueFormat
10624
10677
  };
10625
10678
  }
10626
- function normalizeMapSpec(spec, _warnings) {
10679
+ function normalizeGeoMapSpec(spec, _warnings) {
10627
10680
  return {
10628
10681
  type: "map",
10629
10682
  geo: {
@@ -10675,7 +10728,7 @@ function normalizeSpec(spec, warnings = []) {
10675
10728
  return normalizeTableSpec(spec, warnings);
10676
10729
  }
10677
10730
  if (isGraphSpec(spec)) {
10678
- return normalizeGraphSpec(spec);
10731
+ return normalizeGraphSpec(spec, warnings);
10679
10732
  }
10680
10733
  if (isSankeySpec(spec)) {
10681
10734
  return normalizeSankeySpec(spec, warnings);
@@ -10683,8 +10736,8 @@ function normalizeSpec(spec, warnings = []) {
10683
10736
  if (isTileMapSpec(spec)) {
10684
10737
  return normalizeTileMapSpec(spec, warnings);
10685
10738
  }
10686
- if (isMapSpec(spec)) {
10687
- return normalizeMapSpec(spec, warnings);
10739
+ if (isGeoMapSpec(spec)) {
10740
+ return normalizeGeoMapSpec(spec, warnings);
10688
10741
  }
10689
10742
  if (isBarListSpec(spec)) {
10690
10743
  return normalizeBarListSpec(spec, warnings);
@@ -11000,6 +11053,40 @@ function validateCalendarSpec(spec, errors) {
11000
11053
  seenDays.set(dayKey, i);
11001
11054
  }
11002
11055
  }
11056
+ var MAX_PLAUSIBLE_POINT_RADIUS = 50;
11057
+ function pointSizeError(path2, value2) {
11058
+ const radius = Math.max(1, Math.round(Math.sqrt(value2 / Math.PI)));
11059
+ return {
11060
+ message: `Spec error: ${path2} is a radius in pixels, and ${value2} would draw a ${Math.round(value2 * 2)}px-wide circle. This usually means the value was written in Vega-Lite's units, where point size is an area in px^2.`,
11061
+ path: path2,
11062
+ code: "INVALID_VALUE",
11063
+ suggestion: `Point sizes are radii in px (default 5, typical 2-12). For the dot a Vega-Lite size of ${value2} draws, use ${radius} (r = sqrt(area / pi)).`
11064
+ };
11065
+ }
11066
+ function validatePointSize(spec, errors) {
11067
+ const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
11068
+ if (markType !== "point") return;
11069
+ if (spec.mark && typeof spec.mark === "object") {
11070
+ const size = spec.mark.size;
11071
+ if (typeof size === "number" && size > MAX_PLAUSIBLE_POINT_RADIUS) {
11072
+ errors.push(pointSizeError("mark.size", size));
11073
+ }
11074
+ }
11075
+ const sizeEnc = spec.encoding && typeof spec.encoding === "object" ? spec.encoding.size : void 0;
11076
+ if (!sizeEnc || typeof sizeEnc !== "object") return;
11077
+ if (typeof sizeEnc.value === "number" && sizeEnc.value > MAX_PLAUSIBLE_POINT_RADIUS) {
11078
+ errors.push(pointSizeError("encoding.size.value", sizeEnc.value));
11079
+ }
11080
+ const range3 = sizeEnc.scale && typeof sizeEnc.scale === "object" ? sizeEnc.scale.range : void 0;
11081
+ if (Array.isArray(range3)) {
11082
+ const worst = range3.find(
11083
+ (entry) => typeof entry === "number" && entry > MAX_PLAUSIBLE_POINT_RADIUS
11084
+ );
11085
+ if (typeof worst === "number") {
11086
+ errors.push(pointSizeError("encoding.size.scale.range", worst));
11087
+ }
11088
+ }
11089
+ }
11003
11090
  function validateChartSpec(spec, errors) {
11004
11091
  const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
11005
11092
  if (!Array.isArray(spec.data)) {
@@ -11105,6 +11192,7 @@ function validateChartSpec(spec, errors) {
11105
11192
  if (markType === "calendar") {
11106
11193
  validateCalendarSpec(spec, errors);
11107
11194
  }
11195
+ validatePointSize(spec, errors);
11108
11196
  if (Array.isArray(spec.transform)) {
11109
11197
  const transforms = spec.transform;
11110
11198
  for (let i = 0; i < transforms.length; i++) {
@@ -11317,6 +11405,52 @@ function validateChartSpec(spec, errors) {
11317
11405
  }
11318
11406
  }
11319
11407
  }
11408
+ for (const channel of ["row", "column"]) {
11409
+ const ch = encoding[channel];
11410
+ if (ch && typeof ch === "object") {
11411
+ const facetCh = ch;
11412
+ if (!facetCh.field || typeof facetCh.field !== "string") {
11413
+ errors.push({
11414
+ message: `Spec error: encoding.${channel}.field is required and must be a non-empty string`,
11415
+ path: `encoding.${channel}.field`,
11416
+ code: "MISSING_FIELD",
11417
+ suggestion: `Add a field property to encoding.${channel} (e.g. { field: "category", type: "nominal" })`
11418
+ });
11419
+ } else if (!dataColumns.has(facetCh.field) && !transformFields.has(facetCh.field)) {
11420
+ errors.push({
11421
+ message: `Spec error: encoding.${channel}.field "${facetCh.field}" does not exist in data. Available columns: ${availableColumns}`,
11422
+ path: `encoding.${channel}.field`,
11423
+ code: "DATA_FIELD_MISSING",
11424
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(facetCh.field, columnList)}`
11425
+ });
11426
+ }
11427
+ if (!facetCh.type || facetCh.type !== "nominal" && facetCh.type !== "ordinal") {
11428
+ errors.push({
11429
+ message: `Spec error: encoding.${channel}.type must be "nominal" or "ordinal"${facetCh.type ? `, got "${facetCh.type}"` : ""}`,
11430
+ path: `encoding.${channel}.type`,
11431
+ code: facetCh.type ? "INVALID_VALUE" : "MISSING_FIELD",
11432
+ suggestion: `Set encoding.${channel}.type to "nominal" or "ordinal"`
11433
+ });
11434
+ }
11435
+ if (encoding.facet && typeof encoding.facet === "object") {
11436
+ errors.push({
11437
+ message: `Spec error: encoding.${channel} and encoding.facet cannot be used together`,
11438
+ path: `encoding.${channel}`,
11439
+ code: "INVALID_VALUE",
11440
+ suggestion: `Use either encoding.${channel} or encoding.facet, not both`
11441
+ });
11442
+ }
11443
+ const otherChannel = channel === "row" ? "column" : "row";
11444
+ if (encoding[otherChannel]) {
11445
+ errors.push({
11446
+ message: "Spec error: encoding.row and encoding.column cannot be used together (cross-product faceting is not yet supported)",
11447
+ path: `encoding.${channel}`,
11448
+ code: "INVALID_VALUE",
11449
+ suggestion: "Use encoding.facet with columns for grid layout, or use only row or column"
11450
+ });
11451
+ }
11452
+ }
11453
+ }
11320
11454
  if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
11321
11455
  errors.push({
11322
11456
  message: `Spec error: darkMode must be "auto", "force", or "off"`,
@@ -11496,6 +11630,35 @@ function validateGraphSpec(spec, errors) {
11496
11630
  nodeIds.add(node.id);
11497
11631
  }
11498
11632
  }
11633
+ if (spec.seedNode !== void 0) {
11634
+ if (typeof spec.seedNode === "string") {
11635
+ if (spec.seedNode === "") {
11636
+ errors.push({
11637
+ message: 'Spec error: "seedNode" must be a non-empty node id',
11638
+ path: "seedNode",
11639
+ code: "MISSING_FIELD",
11640
+ suggestion: `Use one of the existing node ids, e.g. seedNode: "${[...nodeIds][0] ?? "a"}"`
11641
+ });
11642
+ }
11643
+ } else if (typeof spec.seedNode !== "object" || spec.seedNode === null || Array.isArray(spec.seedNode)) {
11644
+ errors.push({
11645
+ message: `Spec error: "seedNode" must be a node id string or an object like { id, style }, got ${Array.isArray(spec.seedNode) ? "array" : typeof spec.seedNode}`,
11646
+ path: "seedNode",
11647
+ code: "INVALID_TYPE",
11648
+ suggestion: `Use the id shorthand (seedNode: "${[...nodeIds][0] ?? "a"}") or the object form (seedNode: { id: "${[...nodeIds][0] ?? "a"}", style: { radius: 14 } })`
11649
+ });
11650
+ } else {
11651
+ const seedId = spec.seedNode.id;
11652
+ if (typeof seedId !== "string" || seedId === "") {
11653
+ errors.push({
11654
+ message: 'Spec error: "seedNode" must have a non-empty string "id" field',
11655
+ path: "seedNode.id",
11656
+ code: "MISSING_FIELD",
11657
+ suggestion: `Add the seed node id, e.g. seedNode: { id: "${[...nodeIds][0] ?? "a"}" }`
11658
+ });
11659
+ }
11660
+ }
11661
+ }
11499
11662
  if (!Array.isArray(spec.edges)) {
11500
11663
  errors.push({
11501
11664
  message: 'Spec error: graph spec requires an "edges" array',
@@ -11550,14 +11713,28 @@ function validateGraphSpec(spec, errors) {
11550
11713
  }
11551
11714
  if (spec.encoding && typeof spec.encoding === "object") {
11552
11715
  const encoding = spec.encoding;
11553
- const firstNode = nodes[0];
11554
- const firstEdge = edges.length > 0 ? edges[0] : null;
11555
- const nodeFields = firstNode ? new Set(Object.keys(firstNode)) : /* @__PURE__ */ new Set();
11556
- const edgeFields = firstEdge ? new Set(Object.keys(firstEdge)) : /* @__PURE__ */ new Set();
11557
- const nodeChannels = ["nodeColor", "nodeSize", "nodeLabel"];
11716
+ const nodeFields = /* @__PURE__ */ new Set();
11717
+ for (const node of nodes) {
11718
+ if (node && typeof node === "object") {
11719
+ for (const key of Object.keys(node)) nodeFields.add(key);
11720
+ }
11721
+ }
11722
+ const edgeFields = /* @__PURE__ */ new Set();
11723
+ for (const edge of edges) {
11724
+ if (edge && typeof edge === "object") {
11725
+ for (const key of Object.keys(edge)) edgeFields.add(key);
11726
+ }
11727
+ }
11728
+ const nodeChannels = [
11729
+ "nodeColor",
11730
+ "nodeSize",
11731
+ "nodeOpacity",
11732
+ "nodeLabel",
11733
+ "nodeLabelPriority"
11734
+ ];
11558
11735
  for (const channel of nodeChannels) {
11559
11736
  const ch = encoding[channel];
11560
- if (ch?.field && typeof ch.field === "string" && !nodeFields.has(ch.field)) {
11737
+ if (ch?.field && typeof ch.field === "string" && nodes.length > 0 && !nodeFields.has(ch.field)) {
11561
11738
  errors.push({
11562
11739
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
11563
11740
  path: `encoding.${channel}.field`,
@@ -11566,10 +11743,10 @@ function validateGraphSpec(spec, errors) {
11566
11743
  });
11567
11744
  }
11568
11745
  }
11569
- const edgeChannels = ["edgeColor", "edgeWidth"];
11746
+ const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
11570
11747
  for (const channel of edgeChannels) {
11571
11748
  const ch = encoding[channel];
11572
- if (ch?.field && typeof ch.field === "string" && firstEdge && !edgeFields.has(ch.field)) {
11749
+ if (ch?.field && typeof ch.field === "string" && edges.length > 0 && !edgeFields.has(ch.field)) {
11573
11750
  errors.push({
11574
11751
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
11575
11752
  path: `encoding.${channel}.field`,
@@ -11782,7 +11959,7 @@ function validateTileMapSpec(spec, errors) {
11782
11959
  });
11783
11960
  }
11784
11961
  }
11785
- function validateMapSpec(spec, errors) {
11962
+ function validateGeoMapSpec(spec, errors) {
11786
11963
  if (!spec.geo || typeof spec.geo !== "object") {
11787
11964
  errors.push({
11788
11965
  message: 'Spec error: map spec requires a "geo" object with a TopoJSON "features" field',
@@ -12162,6 +12339,22 @@ function validateLayerSpec(spec, errors) {
12162
12339
  }
12163
12340
  }
12164
12341
  }
12342
+ function validateAnimation(spec, errors) {
12343
+ const animation = spec.animation;
12344
+ if (!animation || typeof animation !== "object" || Array.isArray(animation)) return;
12345
+ const update = animation.update;
12346
+ if (!update || typeof update !== "object" || Array.isArray(update)) return;
12347
+ const maxMarks = update.maxMarks;
12348
+ if (maxMarks === void 0) return;
12349
+ if (typeof maxMarks !== "number" || !Number.isFinite(maxMarks) || maxMarks < 1) {
12350
+ errors.push({
12351
+ message: `Spec error: animation.update.maxMarks must be a number >= 1, got ${JSON.stringify(maxMarks)}`,
12352
+ path: "animation.update.maxMarks",
12353
+ code: "INVALID_VALUE",
12354
+ 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."
12355
+ });
12356
+ }
12357
+ }
12165
12358
  function validateSpec(spec) {
12166
12359
  const errors = [];
12167
12360
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
@@ -12266,10 +12459,13 @@ function validateSpec(spec) {
12266
12459
  } else if (isTileMap) {
12267
12460
  validateTileMapSpec(obj, errors);
12268
12461
  } else if (isMap) {
12269
- validateMapSpec(obj, errors);
12462
+ validateGeoMapSpec(obj, errors);
12270
12463
  } else if (isBarList) {
12271
12464
  validateBarListSpec(obj, errors);
12272
12465
  }
12466
+ if (!isGraph) {
12467
+ validateAnimation(obj, errors);
12468
+ }
12273
12469
  if (errors.length > 0) {
12274
12470
  return { valid: false, errors, normalized: null };
12275
12471
  }
@@ -12293,6 +12489,36 @@ ${errorMessages}`);
12293
12489
  return { spec: normalized, warnings };
12294
12490
  }
12295
12491
 
12492
+ // src/layout/shared.ts
12493
+ function chromeToInput(chrome) {
12494
+ return {
12495
+ eyebrow: chrome.eyebrow,
12496
+ title: chrome.title,
12497
+ subtitle: chrome.subtitle,
12498
+ source: chrome.source,
12499
+ byline: chrome.byline,
12500
+ footer: chrome.footer,
12501
+ brand: chrome.brand
12502
+ };
12503
+ }
12504
+ function scalePadding(basePadding, width, height) {
12505
+ const minDim = Math.min(width, height);
12506
+ if (minDim >= 500) return basePadding;
12507
+ if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
12508
+ const t = (minDim - 200) / 300;
12509
+ return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
12510
+ }
12511
+ function bottomMargin(bottomHeight, padding, xAxisHeight) {
12512
+ return padding + bottomHeight + xAxisHeight;
12513
+ }
12514
+ var INLINE_TICK_OVERHANG_PAD = 6;
12515
+ function resolveChromeLayout(spec, options) {
12516
+ const encoding = spec?.encoding;
12517
+ const isFaceted = !!(encoding?.facet || encoding?.row || encoding?.column);
12518
+ if (isFaceted) return "subtract";
12519
+ return spec?.chromeLayout ?? options?.chromeLayout ?? "subtract";
12520
+ }
12521
+
12296
12522
  // src/barlist/compile-barlist.ts
12297
12523
  var DEFAULT_ROW_GAP = 8;
12298
12524
  var LABEL_BAR_GAP = 12;
@@ -12340,14 +12566,19 @@ function compileBarList(spec, options) {
12340
12566
  watermark
12341
12567
  );
12342
12568
  const padding = theme.spacing.padding;
12569
+ const chromeLayout = resolveChromeLayout(
12570
+ spec,
12571
+ options
12572
+ );
12573
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
12343
12574
  const fullArea = {
12344
12575
  x: padding,
12345
12576
  y: padding + chrome.topHeight,
12346
12577
  width: options.width - padding * 2,
12347
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12578
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12348
12579
  };
12349
12580
  if (fullArea.width <= 0 || fullArea.height <= 0) {
12350
- return emptyLayout2(chrome, theme, options, watermark);
12581
+ return emptyLayout2(chrome, theme, { ...options, height: grownHeight }, watermark);
12351
12582
  }
12352
12583
  const labelField = barlistSpec.encoding.label.field;
12353
12584
  const valueField = barlistSpec.encoding.value.field;
@@ -12532,7 +12763,7 @@ function compileBarList(spec, options) {
12532
12763
  a11y,
12533
12764
  theme,
12534
12765
  width: options.width,
12535
- height: options.height,
12766
+ height: grownHeight,
12536
12767
  animation: resolvedAnimation,
12537
12768
  watermark,
12538
12769
  measureText
@@ -13317,9 +13548,9 @@ function buildBandScale(channel, data, rangeStart, rangeEnd, defaultPadding = DE
13317
13548
  }
13318
13549
  return { scale, type: "band", channel };
13319
13550
  }
13320
- function buildPointScale(channel, data, rangeStart, rangeEnd) {
13551
+ function buildPointScale(channel, data, rangeStart, rangeEnd, defaultPadding = 0.5) {
13321
13552
  const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
13322
- const padding = channel.scale?.padding ?? channel.scale?.paddingOuter ?? 0.5;
13553
+ const padding = channel.scale?.padding ?? channel.scale?.paddingOuter ?? defaultPadding;
13323
13554
  const scale = point().domain(values).range([rangeStart, rangeEnd]).padding(padding);
13324
13555
  if (channel.scale?.reverse) {
13325
13556
  const [r0, r1] = scale.range();
@@ -13396,7 +13627,7 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
13396
13627
  case "point":
13397
13628
  return buildPointScale(channel, data, rangeStart, rangeEnd);
13398
13629
  case "ordinal":
13399
- return buildBandScale(channel, data, rangeStart, rangeEnd);
13630
+ return buildCategoricalScale(channel, data, rangeStart, rangeEnd, chartType, axis);
13400
13631
  }
13401
13632
  }
13402
13633
  switch (channel.type) {
@@ -13406,17 +13637,21 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
13406
13637
  return buildLinearScale(channel, data, rangeStart, rangeEnd);
13407
13638
  case "nominal":
13408
13639
  case "ordinal":
13409
- if (chartType === "rect") {
13410
- return buildBandScale(channel, data, rangeStart, rangeEnd, HEATMAP_BAND_PADDING);
13411
- }
13412
- if (chartType === "bar" || chartType === "beeswarm" || chartType === "range" || (chartType === "circle" || chartType === "lollipop") && axis === "y") {
13413
- return buildBandScale(channel, data, rangeStart, rangeEnd);
13414
- }
13415
- return buildPointScale(channel, data, rangeStart, rangeEnd);
13640
+ return buildCategoricalScale(channel, data, rangeStart, rangeEnd, chartType, axis);
13416
13641
  default:
13417
13642
  return buildLinearScale(channel, data, rangeStart, rangeEnd);
13418
13643
  }
13419
13644
  }
13645
+ function buildCategoricalScale(channel, data, rangeStart, rangeEnd, chartType, axis) {
13646
+ if (chartType === "rect") {
13647
+ return buildBandScale(channel, data, rangeStart, rangeEnd, HEATMAP_BAND_PADDING);
13648
+ }
13649
+ if (chartType === "bar" || chartType === "beeswarm" || chartType === "range" || (chartType === "circle" || chartType === "lollipop") && axis === "y") {
13650
+ return buildBandScale(channel, data, rangeStart, rangeEnd);
13651
+ }
13652
+ const isTrendMark = chartType === "line" || chartType === "area";
13653
+ return buildPointScale(channel, data, rangeStart, rangeEnd, isTrendMark ? 0 : 0.5);
13654
+ }
13420
13655
  function computeScales(spec, chartArea, data) {
13421
13656
  const result = {};
13422
13657
  const encoding = spec.encoding;
@@ -13660,6 +13895,7 @@ var LEGEND_PADDING = 8;
13660
13895
  var LEGEND_RIGHT_WIDTH = 120;
13661
13896
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
13662
13897
  var TOP_LEGEND_MAX_ROWS = 2;
13898
+ var ARC_LABEL_CROWDING_LIMIT = 21;
13663
13899
  function hasLegendContent(content) {
13664
13900
  return content.entries.length > 0 || content.continuous != null;
13665
13901
  }
@@ -13817,6 +14053,12 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
13817
14053
  }
13818
14054
  }
13819
14055
  }
14056
+ const density = spec.labels.density;
14057
+ const everySliceLabeled = density === "all" || density === "auto";
14058
+ const uncrowded = seriesCount > 0 && seriesCount <= ARC_LABEL_CROWDING_LIMIT;
14059
+ if (spec.markType === "arc" && everySliceLabeled && uncrowded && !spec.userExplicit?.legend) {
14060
+ entries = [];
14061
+ }
13820
14062
  const labelStyle = {
13821
14063
  fontFamily: theme.fonts.family,
13822
14064
  fontSize: theme.fonts.sizes.small,
@@ -14561,6 +14803,32 @@ function applyBandLabelStride(ticks2, stride) {
14561
14803
  const lastIdx = ticks2.length - 1;
14562
14804
  return ticks2.filter((_, i) => (lastIdx - i) % stride === 0);
14563
14805
  }
14806
+ var LABEL_EDGE_GUARD = 2;
14807
+ function clampXTickLabels(ticks2, totalWidth, tickAngle, fontSize, fontWeight, measureText) {
14808
+ const rotated = tickAngle !== void 0 && Math.abs(tickAngle) > 10;
14809
+ const rad = rotated ? Math.abs(tickAngle) * Math.PI / 180 : 0;
14810
+ const across = rotated ? fontSize * 1.2 * Math.sin(rad) / 2 : 0;
14811
+ return ticks2.map((tick) => {
14812
+ const width = measureLabel(tick.label, fontSize, fontWeight, measureText);
14813
+ let left2;
14814
+ let right2;
14815
+ if (rotated) {
14816
+ const along = width * Math.cos(rad);
14817
+ const trailsLeft = tickAngle < 0;
14818
+ left2 = trailsLeft ? -(along + across) : -across;
14819
+ right2 = trailsLeft ? across : along + across;
14820
+ } else {
14821
+ left2 = -width / 2;
14822
+ right2 = width / 2;
14823
+ }
14824
+ let x2 = tick.position;
14825
+ const overRight = x2 + right2 - (totalWidth - LABEL_EDGE_GUARD);
14826
+ if (overRight > 0) x2 -= overRight;
14827
+ const overLeft = LABEL_EDGE_GUARD - (x2 + left2);
14828
+ if (overLeft > 0) x2 += overLeft;
14829
+ return x2 === tick.position ? tick : { ...tick, labelPosition: x2 };
14830
+ });
14831
+ }
14564
14832
  function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
14565
14833
  const result = {};
14566
14834
  const baseDensity = strategy.axisLabelDensity;
@@ -14773,6 +15041,14 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
14773
15041
  const totalWidth = dataContext?.totalWidth ?? chartArea.x + chartArea.width;
14774
15042
  if (result.x) {
14775
15043
  const isRotated = !!result.x.tickAngle && Math.abs(result.x.tickAngle) > 10;
15044
+ result.x.ticks = clampXTickLabels(
15045
+ result.x.ticks,
15046
+ totalWidth,
15047
+ result.x.tickAngle,
15048
+ result.x.tickLabelStyle.fontSize,
15049
+ result.x.tickLabelStyle.fontWeight,
15050
+ measureText
15051
+ );
14776
15052
  result.x.extent = computeXAxisExtentFromLabels({
14777
15053
  labels: result.x.ticks.map((t) => t.label),
14778
15054
  tickAngle: result.x.tickAngle,
@@ -14826,30 +15102,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
14826
15102
  return result;
14827
15103
  }
14828
15104
 
14829
- // src/layout/shared.ts
14830
- function chromeToInput(chrome) {
14831
- return {
14832
- eyebrow: chrome.eyebrow,
14833
- title: chrome.title,
14834
- subtitle: chrome.subtitle,
14835
- source: chrome.source,
14836
- byline: chrome.byline,
14837
- footer: chrome.footer,
14838
- brand: chrome.brand
14839
- };
14840
- }
14841
- function scalePadding(basePadding, width, height) {
14842
- const minDim = Math.min(width, height);
14843
- if (minDim >= 500) return basePadding;
14844
- if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
14845
- const t = (minDim - 200) / 300;
14846
- return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
14847
- }
14848
- function bottomMargin(bottomHeight, padding, xAxisHeight) {
14849
- return padding + bottomHeight + xAxisHeight;
14850
- }
14851
- var INLINE_TICK_OVERHANG_PAD = 6;
14852
-
14853
15105
  // src/layout/plan.ts
14854
15106
  function createMeasureFn(measureText) {
14855
15107
  if (measureText) {
@@ -15071,7 +15323,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
15071
15323
  if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
15072
15324
  topMargin += effectiveTopAxisGap;
15073
15325
  const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
15074
- const chartHeight = Math.max(0, height - topMargin - bMargin);
15326
+ const chromeLayout = resolveChromeLayout(renderSpec, options);
15327
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
15328
+ const chartHeight = Math.max(0, effectiveHeight - topMargin - bMargin);
15075
15329
  const rightMargin = hPad + (isRadial ? hPad : axisMargin);
15076
15330
  const rightAxisReserve = options.rightAxisReserve ?? 0;
15077
15331
  const provisionalArea = {
@@ -15223,11 +15477,12 @@ function compileLayer(spec, options, compileChart2) {
15223
15477
  const singleSpec = buildPrimarySpec(leaves, spec);
15224
15478
  return compileChart2(singleSpec, options);
15225
15479
  }
15480
+ const layerOptions = { ...options, layered: true };
15226
15481
  if (spec.resolve?.scale?.y === "independent") {
15227
- return compileLayerIndependent(leaves, spec, options, compileChart2);
15482
+ return compileLayerIndependent(leaves, spec, layerOptions, compileChart2);
15228
15483
  }
15229
15484
  const primarySpec = buildPrimarySpec(leaves, spec);
15230
- const primaryLayout = compileChart2(primarySpec, options);
15485
+ const primaryLayout = compileChart2(primarySpec, layerOptions);
15231
15486
  const allMarks = [];
15232
15487
  const seenLabels = /* @__PURE__ */ new Set();
15233
15488
  const pLegend = primaryLayout.legend;
@@ -15240,7 +15495,7 @@ function compileLayer(spec, options, compileChart2) {
15240
15495
  zIndex: leaf.zIndex ?? i
15241
15496
  }));
15242
15497
  indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
15243
- const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
15498
+ const leafOptions = { ...layerOptions, frozenChartArea: primaryLayout.area };
15244
15499
  const sharedDomains = computeSharedDomains(leaves);
15245
15500
  for (const { leaf } of indexedLeaves) {
15246
15501
  const themedLeaf = {
@@ -15349,11 +15604,9 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
15349
15604
  };
15350
15605
  const xField0 = leaf0.encoding?.x?.field;
15351
15606
  const xField1 = leaf1.encoding?.x?.field;
15352
- const unionXValues = /* @__PURE__ */ new Set();
15353
- if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
15354
- if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
15355
- let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
15356
- let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
15607
+ const union = withUnionXDomain(leaf0, leaf1);
15608
+ let leaf0WithUnionX = union.leaf0;
15609
+ let leaf1WithUnionX = union.leaf1;
15357
15610
  const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
15358
15611
  if (aligned) {
15359
15612
  leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
@@ -15500,20 +15753,75 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
15500
15753
  tooltipDescriptors: mergedTooltips
15501
15754
  };
15502
15755
  }
15503
- function ensureXDomainCoverage(leaf, xField, allXValues) {
15504
- if (!xField || allXValues.size === 0) return leaf;
15505
- const existingXValues = /* @__PURE__ */ new Set();
15506
- for (const row of leaf.data) existingXValues.add(row[xField]);
15507
- const missingRows = [];
15508
- for (const xVal of allXValues) {
15509
- if (!existingXValues.has(xVal)) {
15510
- missingRows.push({ [xField]: xVal });
15756
+ function withXDomain(leaf, domain) {
15757
+ if (!leaf.encoding?.x) return leaf;
15758
+ return {
15759
+ ...leaf,
15760
+ encoding: {
15761
+ ...leaf.encoding,
15762
+ x: {
15763
+ ...leaf.encoding.x,
15764
+ scale: {
15765
+ ...leaf.encoding.x.scale,
15766
+ domain
15767
+ }
15768
+ }
15511
15769
  }
15770
+ };
15771
+ }
15772
+ function withUnionXDomain(leaf0, leaf1) {
15773
+ const xEnc0 = leaf0.encoding?.x;
15774
+ const xEnc1 = leaf1.encoding?.x;
15775
+ const xField0 = xEnc0?.field;
15776
+ const xField1 = xEnc1?.field;
15777
+ if (!xField0 || !xField1) return { leaf0, leaf1 };
15778
+ const xType = xEnc0?.type ?? xEnc1?.type;
15779
+ const pinned0 = xEnc0?.scale?.domain !== void 0;
15780
+ const pinned1 = xEnc1?.scale?.domain !== void 0;
15781
+ if (pinned0 && pinned1) return { leaf0, leaf1 };
15782
+ const values0 = leaf0.data.map((row) => row[xField0]);
15783
+ const values1 = leaf1.data.map((row) => row[xField1]);
15784
+ const allValues = [...values0, ...values1].filter((v) => v != null);
15785
+ if (allValues.length === 0) return { leaf0, leaf1 };
15786
+ let domain;
15787
+ if (xType === "temporal") {
15788
+ let minMs = Number.POSITIVE_INFINITY;
15789
+ let maxMs = Number.NEGATIVE_INFINITY;
15790
+ for (const v of allValues) {
15791
+ const ms = (v instanceof Date ? v : new Date(String(v))).getTime();
15792
+ if (Number.isNaN(ms)) continue;
15793
+ if (ms < minMs) minMs = ms;
15794
+ if (ms > maxMs) maxMs = ms;
15795
+ }
15796
+ if (minMs <= maxMs) {
15797
+ domain = [new Date(minMs).toISOString(), new Date(maxMs).toISOString()];
15798
+ }
15799
+ } else if (xType === "quantitative") {
15800
+ let min4 = Number.POSITIVE_INFINITY;
15801
+ let max4 = Number.NEGATIVE_INFINITY;
15802
+ for (const v of allValues) {
15803
+ const n = typeof v === "number" ? v : Number(v);
15804
+ if (!Number.isFinite(n)) continue;
15805
+ if (n < min4) min4 = n;
15806
+ if (n > max4) max4 = n;
15807
+ }
15808
+ if (min4 <= max4) domain = [min4, max4];
15809
+ } else {
15810
+ const seen = /* @__PURE__ */ new Set();
15811
+ const categories = [];
15812
+ for (const v of allValues) {
15813
+ const s = String(v);
15814
+ if (!seen.has(s)) {
15815
+ seen.add(s);
15816
+ categories.push(s);
15817
+ }
15818
+ }
15819
+ if (categories.length > 0) domain = categories;
15512
15820
  }
15513
- if (missingRows.length === 0) return leaf;
15821
+ if (domain === void 0) return { leaf0, leaf1 };
15514
15822
  return {
15515
- ...leaf,
15516
- data: [...leaf.data, ...missingRows]
15823
+ leaf0: pinned0 ? leaf0 : withXDomain(leaf0, domain),
15824
+ leaf1: pinned1 ? leaf1 : withXDomain(leaf1, domain)
15517
15825
  };
15518
15826
  }
15519
15827
  function alignYDomains(leaf0, leaf1) {
@@ -15736,7 +16044,7 @@ function resolveYouDrawIt(config, marks, scales, chartArea, xField) {
15736
16044
  }
15737
16045
 
15738
16046
  // src/compiler/a11y-warnings.ts
15739
- import { contrastRatio, findAccessibleColor, isOpaqueColor } from "@opendata-ai/openchart-core";
16047
+ import { contrastRatio, findAccessibleColor, isOpaqueColor as isOpaqueColor2 } from "@opendata-ai/openchart-core";
15740
16048
  var GRAPHIC_MIN = 3;
15741
16049
  var TEXT_MIN = 4.5;
15742
16050
  var ADJACENT_FILL_MARKS = /* @__PURE__ */ new Set(["bar", "area", "arc", "waffle"]);
@@ -15744,7 +16052,7 @@ var fmt = (ratio) => `${ratio.toFixed(2)}:1`;
15744
16052
  function collectContrastWarnings(scales, markType, theme) {
15745
16053
  const warnings = [];
15746
16054
  const bg = theme.colors.background;
15747
- const bgIsCheckable = isOpaqueColor(bg);
16055
+ const bgIsCheckable = isOpaqueColor2(bg);
15748
16056
  const series = [];
15749
16057
  if (scales.color && scales.color.type !== "sequential") {
15750
16058
  const ordinal2 = scales.color.scale;
@@ -15792,6 +16100,37 @@ function collectContrastWarnings(scales, markType, theme) {
15792
16100
  return warnings;
15793
16101
  }
15794
16102
 
16103
+ // src/compiler/mark-render-mode.ts
16104
+ var AUTO_CANVAS_THRESHOLD = 1e3;
16105
+ var AUTO_CANVAS_REFUSAL_WARN_THRESHOLD = 5e3;
16106
+ function resolveMarkRenderMode(args, warnings = []) {
16107
+ const { markType, pointCount, display, faceted, layered } = args;
16108
+ const requested = args.requested ?? "auto";
16109
+ if (requested === "svg") return "svg";
16110
+ const refusal = describeRefusal(markType, display, faceted, layered);
16111
+ if (refusal) {
16112
+ if (requested === "canvas") {
16113
+ warnings.push(
16114
+ `Chart warning: renderer "canvas" is not supported ${refusal}; rendering marks as SVG instead.`
16115
+ );
16116
+ } else if (pointCount > AUTO_CANVAS_REFUSAL_WARN_THRESHOLD) {
16117
+ warnings.push(
16118
+ `[openchart] Rendering ${pointCount} point marks as SVG: the canvas mark layer is not supported ${refusal}. Expect slow paint and interaction. Reduce the point count, or restructure so the dense marks compile as a single unlayered, unfaceted point chart.`
16119
+ );
16120
+ }
16121
+ return "svg";
16122
+ }
16123
+ if (requested === "canvas") return "canvas";
16124
+ return pointCount > AUTO_CANVAS_THRESHOLD ? "canvas" : "svg";
16125
+ }
16126
+ function describeRefusal(markType, display, faceted, layered) {
16127
+ if (markType !== "point") return `for ${markType} marks (point marks only)`;
16128
+ if (faceted) return "on faceted charts";
16129
+ if (layered) return "on layered charts";
16130
+ if (display === "sparkline") return "on sparklines";
16131
+ return void 0;
16132
+ }
16133
+
15795
16134
  // src/compiler/sparkline-defaults.ts
15796
16135
  var TREND_DEADBAND = 5e-4;
15797
16136
  function computeTrend(values) {
@@ -15858,7 +16197,7 @@ function buildSparklineAreaGradient(baseColor) {
15858
16197
  };
15859
16198
  }
15860
16199
 
15861
- // src/geo/compile-map.ts
16200
+ // src/geo/compile-geo-map.ts
15862
16201
  import {
15863
16202
  adaptTheme as adaptTheme2,
15864
16203
  buildD3Formatter as buildD3Formatter2,
@@ -17759,7 +18098,7 @@ function identity_default4() {
17759
18098
  return projection2;
17760
18099
  }
17761
18100
 
17762
- // src/geo/compile-map.ts
18101
+ // src/geo/compile-geo-map.ts
17763
18102
  import { feature as topoFeature, mesh as topoMesh } from "topojson-client";
17764
18103
 
17765
18104
  // src/geo/join.ts
@@ -17892,7 +18231,7 @@ function excludePolarFeatures(geojson) {
17892
18231
  return { type: "FeatureCollection", features: filtered };
17893
18232
  }
17894
18233
 
17895
- // src/geo/compile-map.ts
18234
+ // src/geo/compile-geo-map.ts
17896
18235
  var OVERLAY_LEGEND_INSET = 18;
17897
18236
  function validateGeoFeatures(geo) {
17898
18237
  if (!geo.features) {
@@ -17914,14 +18253,14 @@ function validateGeoFeatures(geo) {
17914
18253
  }
17915
18254
  return topo;
17916
18255
  }
17917
- function compileMap(spec, options) {
18256
+ function compileGeoMap(spec, options) {
17918
18257
  const sugarWarnings = [];
17919
18258
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
17920
18259
  const { spec: normalized, warnings } = compile(expandedSpec);
17921
18260
  emitSpecWarnings([...sugarWarnings, ...warnings], options.onWarn);
17922
18261
  if (!("type" in normalized) || normalized.type !== "map") {
17923
18262
  throw new Error(
17924
- "compileMap received a non-map spec. Use compileChart, compileTileMap, compileSankey, or compileBarList instead."
18263
+ "compileGeoMap received a non-map spec. Use compileChart, compileTileMap, compileSankey, or compileBarList instead."
17925
18264
  );
17926
18265
  }
17927
18266
  const mapSpec = normalized;
@@ -17951,11 +18290,15 @@ function compileMap(spec, options) {
17951
18290
  watermark
17952
18291
  );
17953
18292
  const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
18293
+ const chromeLayout = resolveChromeLayout(
18294
+ spec,
18295
+ options
18296
+ );
17954
18297
  const fullArea = {
17955
18298
  x: padding,
17956
18299
  y: padding + chrome.topHeight,
17957
18300
  width: options.width - padding * 2,
17958
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
18301
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
17959
18302
  };
17960
18303
  if (fullArea.width <= 0 || fullArea.height <= 0) {
17961
18304
  return emptyLayout3(chrome, theme, options, watermark);
@@ -18057,7 +18400,7 @@ function compileMap(spec, options) {
18057
18400
  compileWarnings.push(...filteredJoinWarnings);
18058
18401
  }
18059
18402
  const droppedFeatures = [];
18060
- const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
18403
+ const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber3;
18061
18404
  const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
18062
18405
  const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
18063
18406
  let featureMarks;
@@ -18852,6 +19195,65 @@ import {
18852
19195
  resolveTheme as resolveTheme4
18853
19196
  } from "@opendata-ai/openchart-core";
18854
19197
 
19198
+ // src/graphs/animation.ts
19199
+ var ENTER_DEFAULT = {
19200
+ duration: 500,
19201
+ ease: "smooth",
19202
+ stagger: true,
19203
+ cameraFit: true
19204
+ };
19205
+ var UPDATE_DEFAULT = { duration: 300, ease: "smooth" };
19206
+ var EXIT_DEFAULT = { duration: 300, ease: "smooth" };
19207
+ var CAMERA_DEFAULT = { duration: "auto", ease: "smooth" };
19208
+ var HOVER_DEFAULT = { duration: 150, ease: "smooth" };
19209
+ function fullDefaults() {
19210
+ return {
19211
+ enter: { ...ENTER_DEFAULT },
19212
+ update: { ...UPDATE_DEFAULT },
19213
+ exit: { ...EXIT_DEFAULT },
19214
+ camera: { ...CAMERA_DEFAULT },
19215
+ hover: { ...HOVER_DEFAULT }
19216
+ };
19217
+ }
19218
+ function resolveGraphAnimation(spec) {
19219
+ if (spec === void 0 || spec === true) return fullDefaults();
19220
+ if (spec === false) return void 0;
19221
+ return {
19222
+ enter: resolveEnter(spec.enter),
19223
+ update: resolveDurationPhase(spec.update, UPDATE_DEFAULT),
19224
+ exit: resolveDurationPhase(spec.exit, EXIT_DEFAULT),
19225
+ camera: resolveCamera(spec.camera),
19226
+ hover: resolveDurationPhase(spec.hover, HOVER_DEFAULT)
19227
+ };
19228
+ }
19229
+ function resolveEnter(phase) {
19230
+ if (phase === false) return null;
19231
+ if (phase === void 0 || phase === true) return { ...ENTER_DEFAULT };
19232
+ return {
19233
+ duration: phase.duration ?? ENTER_DEFAULT.duration,
19234
+ ease: phase.ease ?? ENTER_DEFAULT.ease,
19235
+ stagger: phase.stagger !== false,
19236
+ cameraFit: ENTER_DEFAULT.cameraFit
19237
+ };
19238
+ }
19239
+ function resolveDurationPhase(phase, defaults) {
19240
+ if (phase === false) return null;
19241
+ if (phase === void 0 || phase === true)
19242
+ return { duration: defaults.duration, ease: defaults.ease };
19243
+ return {
19244
+ duration: phase.duration ?? defaults.duration,
19245
+ ease: phase.ease ?? defaults.ease
19246
+ };
19247
+ }
19248
+ function resolveCamera(phase) {
19249
+ if (phase === false) return null;
19250
+ if (phase === void 0 || phase === true) return { ...CAMERA_DEFAULT };
19251
+ return {
19252
+ duration: phase.duration ?? CAMERA_DEFAULT.duration,
19253
+ ease: phase.ease ?? CAMERA_DEFAULT.ease
19254
+ };
19255
+ }
19256
+
18855
19257
  // src/graphs/encoding.ts
18856
19258
  var DEFAULT_NODE_RADIUS = 5;
18857
19259
  var MIN_NODE_RADIUS = 3;
@@ -18891,16 +19293,42 @@ function computeDegrees(nodes, edges) {
18891
19293
  }
18892
19294
  return degrees2;
18893
19295
  }
19296
+ function resolveCategoricalDomain(values, sort, scaleDomain) {
19297
+ if (Array.isArray(scaleDomain) && scaleDomain.length > 0) {
19298
+ return scaleDomain.map((v) => String(v));
19299
+ }
19300
+ const seen = [];
19301
+ const set = /* @__PURE__ */ new Set();
19302
+ for (const v of values) {
19303
+ if (!set.has(v)) {
19304
+ set.add(v);
19305
+ seen.push(v);
19306
+ }
19307
+ }
19308
+ if (sort === null) return seen;
19309
+ if (Array.isArray(sort)) {
19310
+ const pinned = sort.filter((v) => set.has(v));
19311
+ const pinnedSet = new Set(pinned);
19312
+ const rest = seen.filter((v) => !pinnedSet.has(v));
19313
+ return [...pinned, ...rest];
19314
+ }
19315
+ const sorted = [...seen].sort((a, b) => a.localeCompare(b));
19316
+ return sort === "descending" ? sorted.reverse() : sorted;
19317
+ }
18894
19318
  function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18895
19319
  const degrees2 = computeDegrees(nodes, edges);
18896
19320
  const maxDegree = Math.max(1, ...degrees2.values());
18897
19321
  let sizeScale;
18898
19322
  if (encoding.nodeSize?.field) {
18899
19323
  const field = encoding.nodeSize.field;
19324
+ const scaleConfig = encoding.nodeSize.scale;
18900
19325
  const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
18901
19326
  const sizeMin = min(values) ?? 0;
18902
19327
  const sizeMax = max(values) ?? 1;
18903
- sizeScale = sqrt().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
19328
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [sizeMin, sizeMax];
19329
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [MIN_NODE_RADIUS, MAX_NODE_RADIUS];
19330
+ const ctor = scaleConfig?.type === "linear" ? linear2 : sqrt;
19331
+ sizeScale = ctor().domain(domain).range(range3);
18904
19332
  }
18905
19333
  let colorFn;
18906
19334
  if (encoding.nodeColor?.field) {
@@ -18921,13 +19349,39 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18921
19349
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
18922
19350
  };
18923
19351
  } else {
18924
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
19352
+ const domain = resolveCategoricalDomain(
19353
+ nodes.map((n) => String(n[field] ?? "")),
19354
+ encoding.nodeColor.sort,
19355
+ scaleConfig?.domain
19356
+ );
18925
19357
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18926
19358
  const ordinalScale = ordinal().domain(domain).range(range3);
18927
19359
  colorFn = (node) => ordinalScale(String(node[field] ?? ""));
18928
19360
  }
18929
19361
  }
18930
19362
  const defaultColor = theme.colors.categorical[0];
19363
+ let opacityScale;
19364
+ if (encoding.nodeOpacity?.field) {
19365
+ const field = encoding.nodeOpacity.field;
19366
+ const scaleConfig = encoding.nodeOpacity.scale;
19367
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19368
+ const opMin = min(values) ?? 0;
19369
+ const opMax = max(values) ?? 1;
19370
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [opMin, opMax];
19371
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0.25, 1];
19372
+ opacityScale = linear2().domain(domain).range(range3);
19373
+ }
19374
+ let labelPriorityScale;
19375
+ if (encoding.nodeLabelPriority?.field) {
19376
+ const field = encoding.nodeLabelPriority.field;
19377
+ const scaleConfig = encoding.nodeLabelPriority.scale;
19378
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19379
+ const lpMin = min(values) ?? 0;
19380
+ const lpMax = max(values) ?? 1;
19381
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [lpMin, lpMax];
19382
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0, 1];
19383
+ labelPriorityScale = linear2().domain(domain).range(range3);
19384
+ }
18931
19385
  return nodes.map((node) => {
18932
19386
  let radius = DEFAULT_NODE_RADIUS;
18933
19387
  if (sizeScale && encoding.nodeSize?.field) {
@@ -18936,6 +19390,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18936
19390
  radius = sizeScale(val);
18937
19391
  }
18938
19392
  }
19393
+ let opacity = 1;
19394
+ if (opacityScale && encoding.nodeOpacity?.field) {
19395
+ const val = Number(node[encoding.nodeOpacity.field]);
19396
+ if (Number.isFinite(val)) {
19397
+ opacity = opacityScale(val);
19398
+ }
19399
+ }
18939
19400
  const fill = colorFn ? colorFn(node) : defaultColor;
18940
19401
  const stroke = darkenColor(fill);
18941
19402
  let label;
@@ -18945,8 +19406,14 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18945
19406
  } else {
18946
19407
  label = node.id;
18947
19408
  }
18948
- const degree = degrees2.get(node.id) ?? 0;
18949
- const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19409
+ let labelPriority;
19410
+ if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
19411
+ const val = Number(node[encoding.nodeLabelPriority.field]);
19412
+ labelPriority = Number.isFinite(val) ? labelPriorityScale(val) : 0;
19413
+ } else {
19414
+ const degree = degrees2.get(node.id) ?? 0;
19415
+ labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19416
+ }
18950
19417
  const { id: _id, ...rest } = node;
18951
19418
  const data = { id: node.id, ...rest };
18952
19419
  const override = nodeOverrides?.[node.id];
@@ -18964,6 +19431,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18964
19431
  label,
18965
19432
  labelPriority: finalLabelPriority,
18966
19433
  community: void 0,
19434
+ opacity,
18967
19435
  data
18968
19436
  };
18969
19437
  });
@@ -18996,7 +19464,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
18996
19464
  return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
18997
19465
  };
18998
19466
  } else {
18999
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
19467
+ const domain = resolveCategoricalDomain(
19468
+ edges.map((e) => String(e[field] ?? "")),
19469
+ encoding.edgeColor.sort,
19470
+ scaleConfig?.domain
19471
+ );
19000
19472
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
19001
19473
  const ordinalScale = ordinal().domain(domain).range(range3);
19002
19474
  edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
@@ -19007,10 +19479,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19007
19479
  let styleFn;
19008
19480
  if (encoding.edgeStyle?.field) {
19009
19481
  const field = encoding.edgeStyle.field;
19010
- const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
19482
+ const domain = resolveCategoricalDomain(
19483
+ edges.map((e) => String(e[field] ?? "")),
19484
+ encoding.edgeStyle.sort,
19485
+ encoding.edgeStyle.scale?.domain
19486
+ );
19011
19487
  const styleMap = /* @__PURE__ */ new Map();
19012
- for (let i = 0; i < uniqueValues.length; i++) {
19013
- styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19488
+ for (let i = 0; i < domain.length; i++) {
19489
+ styleMap.set(domain[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19014
19490
  }
19015
19491
  styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
19016
19492
  }
@@ -19068,12 +19544,59 @@ function applyCommunityColors(nodes, colorMap) {
19068
19544
  }
19069
19545
  }
19070
19546
 
19547
+ // src/graphs/interaction.ts
19548
+ var DEFAULT_DIM_OPACITY = 0.15;
19549
+ var DEFAULT_CURSOR_RADIUS = 80;
19550
+ var DEFAULT_CURSOR_STRENGTH = 30;
19551
+ function resolveGraphInteraction(cfg) {
19552
+ const hover = cfg?.hover;
19553
+ const cursor = cfg?.cursorRepulsion;
19554
+ let cursorRepulsion = null;
19555
+ if (cursor === true) {
19556
+ cursorRepulsion = { radius: DEFAULT_CURSOR_RADIUS, strength: DEFAULT_CURSOR_STRENGTH };
19557
+ } else if (cursor && typeof cursor === "object") {
19558
+ cursorRepulsion = {
19559
+ radius: cursor.radius ?? DEFAULT_CURSOR_RADIUS,
19560
+ strength: cursor.strength ?? DEFAULT_CURSOR_STRENGTH
19561
+ };
19562
+ }
19563
+ return {
19564
+ hoverMode: hover?.mode ?? "neighbors",
19565
+ dimOpacity: hover?.dimOpacity ?? DEFAULT_DIM_OPACITY,
19566
+ selectFlyTo: cfg?.select?.flyTo ?? false,
19567
+ cursorRepulsion,
19568
+ springyDrag: cfg?.springyDrag ?? false
19569
+ };
19570
+ }
19571
+
19071
19572
  // src/graphs/compile-graph.ts
19072
19573
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
19574
+ function mergeOverrides(...layers) {
19575
+ const merged = {};
19576
+ for (const layer of layers) {
19577
+ if (!layer) continue;
19578
+ for (const [key, value2] of Object.entries(layer)) {
19579
+ if (value2 !== void 0) merged[key] = value2;
19580
+ }
19581
+ }
19582
+ return merged;
19583
+ }
19073
19584
  var SWATCH_SIZE2 = 12;
19074
19585
  var SWATCH_GAP2 = 6;
19075
19586
  var ENTRY_GAP2 = 16;
19076
- function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
19587
+ var ENERGY_PRESETS = {
19588
+ gentle: { chargeStrength: -150, velocityDecay: 0.5 },
19589
+ balanced: { chargeStrength: -300, velocityDecay: 0.4 },
19590
+ energetic: { chargeStrength: -600, velocityDecay: 0.3 }
19591
+ };
19592
+ var SETTLE_PRESETS = {
19593
+ quick: 0.05,
19594
+ balanced: 0.0228,
19595
+ thorough: 0.01
19596
+ };
19597
+ var DEFAULT_WARMUP_TICKS = 100;
19598
+ var DEFAULT_WARMUP_BUDGET_MS = 250;
19599
+ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField, legendOrder) {
19077
19600
  const labelStyle = {
19078
19601
  fontFamily: theme.fonts.family,
19079
19602
  fontSize: theme.fonts.sizes.small,
@@ -19083,25 +19606,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19083
19606
  };
19084
19607
  let entries;
19085
19608
  if (hasCommunities && communityColorMap.size > 0) {
19609
+ const counts = /* @__PURE__ */ new Map();
19610
+ for (const node of nodes) {
19611
+ if (node.community != null) {
19612
+ counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
19613
+ }
19614
+ }
19086
19615
  entries = [...communityColorMap.entries()].map(([label, color2]) => ({
19087
19616
  label,
19088
19617
  color: color2,
19089
19618
  shape: "circle",
19090
- active: true
19619
+ active: true,
19620
+ count: counts.get(label) ?? 0
19091
19621
  }));
19092
19622
  } else if (nodeColorField) {
19093
19623
  const categoryColors = /* @__PURE__ */ new Map();
19624
+ const categoryCounts = /* @__PURE__ */ new Map();
19094
19625
  for (const node of nodes) {
19095
19626
  const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
19096
19627
  if (!categoryColors.has(category)) {
19097
19628
  categoryColors.set(category, node.fill);
19098
19629
  }
19630
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19099
19631
  }
19100
- entries = categoryColors.size <= 1 ? [] : [...categoryColors.entries()].map(([label, color2]) => ({
19632
+ entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
19101
19633
  label,
19102
- color: color2,
19634
+ color: categoryColors.get(label),
19103
19635
  shape: "circle",
19104
- active: true
19636
+ active: true,
19637
+ count: categoryCounts.get(label) ?? 0
19105
19638
  }));
19106
19639
  } else {
19107
19640
  entries = [];
@@ -19117,6 +19650,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19117
19650
  swatchChipFill: theme.colors.annotationFill
19118
19651
  };
19119
19652
  }
19653
+ function buildEdgeLegend(edges, encoding) {
19654
+ const field = encoding.edgeColor?.field;
19655
+ if (!field || (encoding.edgeColor?.type ?? "nominal") === "quantitative") return void 0;
19656
+ const categoryColors = /* @__PURE__ */ new Map();
19657
+ const categoryCounts = /* @__PURE__ */ new Map();
19658
+ for (const edge of edges) {
19659
+ const category = String(edge.data[field] ?? "");
19660
+ if (!categoryColors.has(category)) categoryColors.set(category, edge.stroke);
19661
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19662
+ }
19663
+ if (categoryColors.size <= 1) return void 0;
19664
+ const order = resolveCategoricalDomain(
19665
+ edges.map((e) => String(e.data[field] ?? "")),
19666
+ encoding.edgeColor?.sort,
19667
+ encoding.edgeColor?.scale?.domain
19668
+ );
19669
+ return order.filter((label) => categoryColors.has(label)).map((label) => ({
19670
+ label,
19671
+ color: categoryColors.get(label),
19672
+ shape: "line",
19673
+ active: true,
19674
+ count: categoryCounts.get(label) ?? 0
19675
+ }));
19676
+ }
19120
19677
  function buildGraphTooltips(nodes) {
19121
19678
  const descriptors = /* @__PURE__ */ new Map();
19122
19679
  for (const node of nodes) {
@@ -19143,6 +19700,21 @@ function buildGraphTooltips(nodes) {
19143
19700
  }
19144
19701
  return descriptors;
19145
19702
  }
19703
+ function buildEdgeTooltip(edge) {
19704
+ const fields = [];
19705
+ for (const [key, value2] of Object.entries(edge.data)) {
19706
+ if (key === "source" || key === "target") continue;
19707
+ if (value2 == null) continue;
19708
+ fields.push({
19709
+ label: key,
19710
+ value: typeof value2 === "number" ? graphNumberFormatter(value2) : String(value2)
19711
+ });
19712
+ }
19713
+ return {
19714
+ title: `${edge.source} \u2192 ${edge.target}`,
19715
+ fields
19716
+ };
19717
+ }
19146
19718
  function compileGraph(spec, options) {
19147
19719
  const { spec: normalized, warnings } = compile(spec);
19148
19720
  emitSpecWarnings(warnings, options.onWarn);
@@ -19152,6 +19724,16 @@ function compileGraph(spec, options) {
19152
19724
  );
19153
19725
  }
19154
19726
  const graphSpec = normalized;
19727
+ for (const [name, ch] of [
19728
+ ["nodeColor", graphSpec.encoding.nodeColor],
19729
+ ["edgeColor", graphSpec.encoding.edgeColor]
19730
+ ]) {
19731
+ if (ch?.sort != null && ch.type === "quantitative") {
19732
+ options.onWarn?.(
19733
+ `encoding.${name}.sort is ignored for a quantitative field (sort only orders categorical domains).`
19734
+ );
19735
+ }
19736
+ }
19155
19737
  const rawWatermark = spec.watermark;
19156
19738
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
19157
19739
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
@@ -19159,12 +19741,23 @@ function compileGraph(spec, options) {
19159
19741
  if (options.darkMode) {
19160
19742
  theme = adaptTheme3(theme);
19161
19743
  }
19744
+ const seedNodeIds = graphSpec.seedNode ? [graphSpec.seedNode.id] : [];
19745
+ const nodeOverrides = graphSpec.seedNode ? {
19746
+ ...graphSpec.nodeOverrides,
19747
+ [graphSpec.seedNode.id]: mergeOverrides(
19748
+ // Radius is deliberately absent so the seed doesn't fight a nodeSize
19749
+ // encoding; a host wanting a bigger seed sets seedNode.style.radius.
19750
+ { stroke: theme.colors.text, strokeWidth: 2, alwaysShowLabel: true },
19751
+ graphSpec.seedNode.style,
19752
+ graphSpec.nodeOverrides?.[graphSpec.seedNode.id]
19753
+ )
19754
+ } : graphSpec.nodeOverrides;
19162
19755
  const compiledNodes = resolveNodeVisuals(
19163
19756
  graphSpec.nodes,
19164
19757
  graphSpec.encoding,
19165
19758
  graphSpec.edges,
19166
19759
  theme,
19167
- graphSpec.nodeOverrides
19760
+ nodeOverrides
19168
19761
  );
19169
19762
  const clusteringField = graphSpec.layout.clustering?.field;
19170
19763
  const hasCommunities = !!clusteringField;
@@ -19177,13 +19770,30 @@ function compileGraph(spec, options) {
19177
19770
  }
19178
19771
  const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
19179
19772
  const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
19773
+ const nodeColorField = graphSpec.encoding.nodeColor?.field;
19774
+ const legendOrder = nodeColorField ? resolveCategoricalDomain(
19775
+ compiledNodes.map((n) => String(n.data[nodeColorField] ?? n.label ?? n.id)),
19776
+ graphSpec.encoding.nodeColor?.sort,
19777
+ graphSpec.encoding.nodeColor?.scale?.domain
19778
+ ) : void 0;
19180
19779
  const legend = buildGraphLegend(
19181
19780
  compiledNodes,
19182
19781
  communityColorMap,
19183
19782
  useCommunitiesForLegend,
19184
19783
  theme,
19185
- graphSpec.encoding.nodeColor?.field
19784
+ nodeColorField,
19785
+ legendOrder
19186
19786
  );
19787
+ const edgeLegend = buildEdgeLegend(compiledEdges, graphSpec.encoding);
19788
+ const legendField = nodeColorField ?? (useCommunitiesForLegend ? clusteringField ?? null : null);
19789
+ let initialHighlight;
19790
+ if (nodeColorField && graphSpec.encoding.nodeColor?.highlight != null) {
19791
+ const raw = graphSpec.encoding.nodeColor.highlight;
19792
+ const requested = Array.isArray(raw) ? raw : [raw];
19793
+ const available = new Set(legendOrder ?? []);
19794
+ const values = requested.filter((v) => available.has(v));
19795
+ if (values.length > 0) initialHighlight = { field: nodeColorField, values };
19796
+ }
19187
19797
  const tooltipDescriptors = buildGraphTooltips(compiledNodes);
19188
19798
  const communityCount = communityColorMap.size;
19189
19799
  const altParts = [
@@ -19200,17 +19810,26 @@ function compileGraph(spec, options) {
19200
19810
  };
19201
19811
  const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
19202
19812
  const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
19813
+ const energyPreset = graphSpec.layout.energy ? ENERGY_PRESETS[graphSpec.layout.energy] : ENERGY_PRESETS.balanced;
19814
+ const settlePreset = graphSpec.layout.settle ? SETTLE_PRESETS[graphSpec.layout.settle] : SETTLE_PRESETS.balanced;
19815
+ const warmupRaw = graphSpec.layout.warmup;
19816
+ const warmupTicks = warmupRaw === false ? 0 : typeof warmupRaw === "number" ? Math.max(0, Math.floor(warmupRaw)) : DEFAULT_WARMUP_TICKS;
19203
19817
  const simulationConfig = {
19204
- chargeStrength: graphSpec.layout.chargeStrength ?? -300,
19818
+ chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
19205
19819
  linkDistance: graphSpec.layout.linkDistance ?? 30,
19206
19820
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
19207
- alphaDecay: 0.0228,
19208
- velocityDecay: 0.4,
19821
+ alphaDecay: settlePreset,
19822
+ velocityDecay: energyPreset.velocityDecay,
19209
19823
  collisionRadius: maxRadius + collisionPadding,
19210
19824
  collisionPadding,
19211
19825
  linkStrength: graphSpec.layout.linkStrength,
19212
- centerForce: graphSpec.layout.centerForce
19826
+ centerForce: graphSpec.layout.centerForce,
19827
+ seed: graphSpec.layout.seed,
19828
+ warmupTicks,
19829
+ warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
19213
19830
  };
19831
+ const animation = resolveGraphAnimation(graphSpec.animation);
19832
+ const interaction = resolveGraphInteraction(graphSpec.interaction);
19214
19833
  const chrome = computeChrome4(
19215
19834
  {
19216
19835
  title: graphSpec.chrome.title,
@@ -19239,7 +19858,13 @@ function compileGraph(spec, options) {
19239
19858
  height: options.height
19240
19859
  },
19241
19860
  simulationConfig,
19242
- watermark
19861
+ watermark,
19862
+ animation,
19863
+ interaction,
19864
+ legendField,
19865
+ initialHighlight,
19866
+ seedNodeIds,
19867
+ edgeLegend
19243
19868
  };
19244
19869
  }
19245
19870
  var DEFAULT_COLLISION_PADDING = 5;
@@ -19332,6 +19957,7 @@ function legendLayoutHasContent(legendLayout) {
19332
19957
  }
19333
19958
  var MIN_CHART_WIDTH = 60;
19334
19959
  var MIN_CHART_HEIGHT2 = 40;
19960
+ var PLOT_SHARE_MAX = 0.4;
19335
19961
  var SERIES_SEARCH_INPUT_HEIGHT = 32;
19336
19962
  var SERIES_SEARCH_GAP = 12;
19337
19963
  function getMinChartDims(display) {
@@ -19374,8 +20000,10 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19374
20000
  watermark,
19375
20001
  bottomLegendReservation
19376
20002
  );
20003
+ const chromeLayout = resolveChromeLayout(spec, options);
20004
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
19377
20005
  if (isSparkline) {
19378
- const total2 = { x: 0, y: 0, width, height };
20006
+ const total2 = { x: 0, y: 0, width, height: effectiveHeight };
19379
20007
  const sparkPad = getSparklinePad(spec);
19380
20008
  const xAxisSpace = userExplicit.xAxis ? 26 : 0;
19381
20009
  const yAxisSpace = userExplicit.yAxis ? 30 : 0;
@@ -19399,7 +20027,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19399
20027
  x: margins2.left,
19400
20028
  y: margins2.top,
19401
20029
  width: Math.max(0, width - margins2.left - margins2.right),
19402
- height: Math.max(0, height - margins2.top - margins2.bottom)
20030
+ height: Math.max(0, effectiveHeight - margins2.top - margins2.bottom)
19403
20031
  };
19404
20032
  return {
19405
20033
  total: total2,
@@ -19411,7 +20039,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19411
20039
  effectiveAxisGap: 0
19412
20040
  };
19413
20041
  }
19414
- const total = { x: 0, y: 0, width, height };
20042
+ const total = { x: 0, y: 0, width, height: effectiveHeight };
19415
20043
  const isRadial = isAxislessMark2(spec.markType);
19416
20044
  const encoding = spec.encoding;
19417
20045
  const xAxisSuppressed = encoding.x?.axis === false;
@@ -19641,10 +20269,11 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19641
20269
  x: margins.left,
19642
20270
  y: margins.top,
19643
20271
  width: Math.max(0, width - margins.left - margins.right),
19644
- height: Math.max(0, height - margins.top - margins.bottom)
20272
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19645
20273
  };
19646
20274
  const minDims = getMinChartDims(spec.display);
19647
- if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
20275
+ const chromeEatsPlot = chromeLayout !== "grow" && chrome.topHeight > height * PLOT_SHARE_MAX;
20276
+ if ((chartArea.width < minDims.width || chartArea.height < minDims.height || chromeEatsPlot) && chromeMode !== "hidden") {
19648
20277
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
19649
20278
  const fallbackChrome = computeChrome5(
19650
20279
  chromeToInput(spec.chrome),
@@ -19670,7 +20299,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19670
20299
  x: margins.left,
19671
20300
  y: margins.top,
19672
20301
  width: Math.max(0, width - margins.left - margins.right),
19673
- height: Math.max(0, height - margins.top - margins.bottom)
20302
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19674
20303
  };
19675
20304
  const fallbackMetricsTopY = topPad + fallbackChrome.topHeight;
19676
20305
  const fallbackMetricsArea = { x: hPad, width: Math.max(0, width - hPad * 2) };
@@ -19687,7 +20316,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19687
20316
  chartArea = {
19688
20317
  ...chartArea,
19689
20318
  y: margins.top,
19690
- height: Math.max(0, height - margins.top - margins.bottom)
20319
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19691
20320
  };
19692
20321
  }
19693
20322
  return {
@@ -19703,7 +20332,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19703
20332
  fallbackMetricsArea
19704
20333
  ) : void 0,
19705
20334
  xAxisHeight,
19706
- effectiveAxisGap: fallbackEffectiveAxisGap
20335
+ effectiveAxisGap: fallbackEffectiveAxisGap,
20336
+ chromeStripped: true
19707
20337
  };
19708
20338
  }
19709
20339
  }
@@ -19715,7 +20345,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19715
20345
  chartArea = {
19716
20346
  ...chartArea,
19717
20347
  y: margins.top,
19718
- height: Math.max(0, height - margins.top - margins.bottom)
20348
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19719
20349
  };
19720
20350
  }
19721
20351
  return {
@@ -20467,14 +21097,19 @@ function compileSankey(spec, options) {
20467
21097
  watermark
20468
21098
  );
20469
21099
  const padding = theme.spacing.padding;
21100
+ const chromeLayout = resolveChromeLayout(
21101
+ spec,
21102
+ options
21103
+ );
21104
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
20470
21105
  const fullArea = {
20471
21106
  x: padding,
20472
21107
  y: padding + chrome.topHeight,
20473
21108
  width: options.width - padding * 2,
20474
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
21109
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
20475
21110
  };
20476
21111
  if (fullArea.width <= 0 || fullArea.height <= 0) {
20477
- return emptyLayout4(fullArea, chrome, theme, options, watermark);
21112
+ return emptyLayout4(fullArea, chrome, theme, { ...options, height: grownHeight }, watermark);
20478
21113
  }
20479
21114
  const sourceField = sankeySpec.encoding.source.field;
20480
21115
  const targetField = sankeySpec.encoding.target.field;
@@ -20514,7 +21149,7 @@ function compileSankey(spec, options) {
20514
21149
  height: fullArea.height - legend.bounds.height - legendGap2
20515
21150
  };
20516
21151
  if (area.height <= 0) {
20517
- return emptyLayout4(area, chrome, theme, options, watermark);
21152
+ return emptyLayout4(area, chrome, theme, { ...options, height: grownHeight }, watermark);
20518
21153
  }
20519
21154
  const labelFontSize = theme.fonts.sizes.small;
20520
21155
  const labelFontWeight = theme.fonts.weights.normal;
@@ -20666,7 +21301,7 @@ function compileSankey(spec, options) {
20666
21301
  theme,
20667
21302
  dimensions: {
20668
21303
  width: options.width,
20669
- height: options.height
21304
+ height: grownHeight
20670
21305
  },
20671
21306
  animation: resolvedAnimation,
20672
21307
  watermark,
@@ -21485,6 +22120,10 @@ var LEGEND_ENTRY_GAP = 16;
21485
22120
  function clamp(value2, min4, max4) {
21486
22121
  return Math.min(max4, Math.max(min4, value2));
21487
22122
  }
22123
+ function growContentHeight(contentHeight, chromeLayout, budgetHeight, chrome) {
22124
+ if (chromeLayout !== "grow") return contentHeight;
22125
+ return Math.max(contentHeight, budgetHeight + chrome.topHeight + chrome.bottomHeight);
22126
+ }
21488
22127
  function compileTileMap(spec, options) {
21489
22128
  const sugarWarnings = [];
21490
22129
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
@@ -21521,22 +22160,44 @@ function compileTileMap(spec, options) {
21521
22160
  watermark
21522
22161
  );
21523
22162
  const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
22163
+ const chromeLayout = resolveChromeLayout(
22164
+ spec,
22165
+ options
22166
+ );
21524
22167
  const fullArea = {
21525
22168
  x: padding,
21526
22169
  y: padding + chrome.topHeight,
21527
22170
  width: options.width - padding * 2,
21528
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
22171
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
21529
22172
  };
21530
22173
  if (fullArea.width <= 0 || fullArea.height <= 0) {
21531
22174
  return emptyLayout5(chrome, theme, options, watermark);
21532
22175
  }
21533
22176
  const isCategorical = tilemapSpec.encoding.color !== void 0 || tilemapSpec.colors !== void 0;
21534
22177
  if (isCategorical) {
21535
- return compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
22178
+ return compileCategorical(
22179
+ tilemapSpec,
22180
+ options,
22181
+ theme,
22182
+ chrome,
22183
+ fullArea,
22184
+ isDarkMode,
22185
+ watermark,
22186
+ chromeLayout
22187
+ );
21536
22188
  }
21537
- return compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
22189
+ return compileQuantitative(
22190
+ tilemapSpec,
22191
+ options,
22192
+ theme,
22193
+ chrome,
22194
+ fullArea,
22195
+ isDarkMode,
22196
+ watermark,
22197
+ chromeLayout
22198
+ );
21538
22199
  }
21539
- function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22200
+ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21540
22201
  const stateField = tilemapSpec.encoding.state.field;
21541
22202
  const valueField = tilemapSpec.encoding.value.field;
21542
22203
  const stateValueMap = /* @__PURE__ */ new Map();
@@ -21637,7 +22298,12 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21637
22298
  };
21638
22299
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21639
22300
  const padding = fullArea.x;
21640
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22301
+ const contentHeight = growContentHeight(
22302
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22303
+ chromeLayout,
22304
+ options.height,
22305
+ chrome
22306
+ );
21641
22307
  return {
21642
22308
  area: fullArea,
21643
22309
  chrome,
@@ -21654,7 +22320,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21654
22320
  measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth23(text, fontSize), height: fontSize }))
21655
22321
  };
21656
22322
  }
21657
- function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22323
+ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21658
22324
  const stateField = tilemapSpec.encoding.state.field;
21659
22325
  const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
21660
22326
  const stateCategoryMap = /* @__PURE__ */ new Map();
@@ -21773,7 +22439,12 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
21773
22439
  };
21774
22440
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21775
22441
  const padding = fullArea.x;
21776
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22442
+ const contentHeight = growContentHeight(
22443
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22444
+ chromeLayout,
22445
+ options.height,
22446
+ chrome
22447
+ );
21777
22448
  return {
21778
22449
  area: fullArea,
21779
22450
  chrome,
@@ -22881,7 +23552,7 @@ function compileChart(spec, optionsInput) {
22881
23552
  throw new Error("compileChart received a sankey spec. Use compileSankey instead.");
22882
23553
  }
22883
23554
  if ("type" in normalized && normalized.type === "map") {
22884
- throw new Error("compileChart received a map spec. Use compileMap instead.");
23555
+ throw new Error("compileChart received a map spec. Use compileGeoMap instead.");
22885
23556
  }
22886
23557
  let chartSpec = normalized;
22887
23558
  const rawWatermark = expandedSpec.watermark;
@@ -23034,6 +23705,10 @@ function compileChart(spec, optionsInput) {
23034
23705
  watermark = false;
23035
23706
  chartSpec = { ...chartSpec, watermark: false };
23036
23707
  }
23708
+ if (heightClass === "cramped" && !chartSpec.userExplicit.watermark) {
23709
+ watermark = false;
23710
+ chartSpec = { ...chartSpec, watermark: false };
23711
+ }
23037
23712
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
23038
23713
  let theme = resolveTheme7(mergedThemeConfig);
23039
23714
  if (options.darkMode) {
@@ -23065,12 +23740,35 @@ function compileChart(spec, optionsInput) {
23065
23740
  }
23066
23741
  };
23067
23742
  }
23743
+ let facetDirection = "wrap";
23744
+ const rowChannel = renderSpec.encoding.row;
23745
+ const colChannel = renderSpec.encoding.column;
23746
+ if (rowChannel?.field || colChannel?.field) {
23747
+ const channel = rowChannel?.field ? rowChannel : colChannel;
23748
+ facetDirection = rowChannel?.field ? "row" : "column";
23749
+ const desugared = { ...renderSpec.encoding };
23750
+ delete desugared.row;
23751
+ delete desugared.column;
23752
+ desugared.facet = {
23753
+ field: channel.field,
23754
+ type: channel.type,
23755
+ sort: channel.sort,
23756
+ ...facetDirection === "row" ? { columns: 1 } : {}
23757
+ };
23758
+ renderSpec = { ...renderSpec, encoding: desugared };
23759
+ const chartDesugared = { ...chartSpec.encoding };
23760
+ delete chartDesugared.row;
23761
+ delete chartDesugared.column;
23762
+ chartDesugared.facet = desugared.facet;
23763
+ chartSpec = { ...chartSpec, encoding: chartDesugared };
23764
+ }
23068
23765
  const facetChannel = renderSpec.encoding.facet;
23069
23766
  if (facetChannel?.field) {
23070
23767
  return compileFaceted(
23071
23768
  chartSpec,
23072
23769
  renderSpec,
23073
23770
  facetChannel,
23771
+ facetDirection,
23074
23772
  options,
23075
23773
  theme,
23076
23774
  strategy,
@@ -23135,7 +23833,10 @@ function compileChart(spec, optionsInput) {
23135
23833
  skipY,
23136
23834
  markType: chartSpec.markType,
23137
23835
  totalWidth: options.width,
23138
- precomputedYTicks: plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23836
+ // If the guardrail stripped chrome, the plan's y-ticks were sized
23837
+ // against the pre-strip (shorter) plot. Drop them so computeAxes
23838
+ // regenerates against the final (taller) chartArea.
23839
+ precomputedYTicks: !dims.chromeStripped && plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23139
23840
  });
23140
23841
  const xAxisExtent = axes.x?.extent ?? 0;
23141
23842
  dims.chrome.bottomAnchorY = chartArea.y + chartArea.height + xAxisExtent;
@@ -23150,6 +23851,19 @@ function compileChart(spec, optionsInput) {
23150
23851
  const renderer = getChartRenderer(rendererKey);
23151
23852
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
23152
23853
  applyFillPatterns(marks, chartSpec.markDef, theme);
23854
+ const markRenderModeWarnings = [];
23855
+ const markRenderMode = resolveMarkRenderMode(
23856
+ {
23857
+ requested: options.renderer,
23858
+ markType: chartSpec.markType,
23859
+ pointCount: marks.reduce((n, m) => m.type === "point" ? n + 1 : n, 0),
23860
+ display: chartSpec.display,
23861
+ faceted: false,
23862
+ layered: options.layered ?? false
23863
+ },
23864
+ markRenderModeWarnings
23865
+ );
23866
+ emitSpecWarnings(markRenderModeWarnings, options.onWarn);
23153
23867
  const xFieldForYdi = chartSpec.encoding.x && "field" in chartSpec.encoding.x ? chartSpec.encoding.x.field : void 0;
23154
23868
  const youDrawIt = resolveYouDrawIt(chartSpec.youDrawIt, marks, scales, chartArea, xFieldForYdi);
23155
23869
  const endpointLabels = computeEndpointLabels(
@@ -23251,6 +23965,7 @@ function compileChart(spec, optionsInput) {
23251
23965
  a11y: {
23252
23966
  altText,
23253
23967
  dataTableFallback,
23968
+ ...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
23254
23969
  role: "img",
23255
23970
  keyboardNavigable: marks.length > 0,
23256
23971
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
@@ -23258,7 +23973,10 @@ function compileChart(spec, optionsInput) {
23258
23973
  theme,
23259
23974
  dimensions: {
23260
23975
  width: options.width,
23261
- height: options.height
23976
+ // In chromeLayout 'grow' mode, computeDimensions grows total.height by the
23977
+ // chrome height so the plot keeps its full budget. In the default
23978
+ // 'subtract' mode total.height === options.height, so this is a no-op.
23979
+ height: dims.total.height
23262
23980
  },
23263
23981
  animation: resolvedAnimation,
23264
23982
  watermark,
@@ -23266,10 +23984,11 @@ function compileChart(spec, optionsInput) {
23266
23984
  crosshair,
23267
23985
  measureText: options.measureText,
23268
23986
  ...xInvert ? { xInvert } : {},
23269
- ...yInvert ? { yInvert } : {}
23987
+ ...yInvert ? { yInvert } : {},
23988
+ ...markRenderMode === "canvas" ? { markRenderMode } : {}
23270
23989
  };
23271
23990
  }
23272
- function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
23991
+ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
23273
23992
  const measure = createMeasureFn(options.measureText);
23274
23993
  const panelEncoding = { ...renderSpec.encoding };
23275
23994
  delete panelEncoding.facet;
@@ -23315,10 +24034,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23315
24034
  plan.xAxisExtent,
23316
24035
  dims.effectiveAxisGap
23317
24036
  );
23318
- const leftReservation = plan.leftGutter;
23319
24037
  const bottomReservation = plan.xAxisExtent;
23320
- const grid = computeFacetGrid(facetValues, facetChannel.columns, chartArea, {
23321
- left: leftReservation,
24038
+ const effectiveColumns = facetDirection === "column" ? facetValues.length : facetChannel.columns;
24039
+ const grid = computeFacetGrid(facetValues, effectiveColumns, chartArea, {
24040
+ left: 0,
23322
24041
  bottom: bottomReservation
23323
24042
  });
23324
24043
  if (!optionsInput.facetHeightGrown && grid.panels.length > 0 && grid.panels[0].area.height < MIN_PANEL_HEIGHT) {
@@ -23333,8 +24052,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23333
24052
  }
23334
24053
  }
23335
24054
  const resolveConfig = chartSpec.resolve;
23336
- const yResolve = resolveConfig?.scale?.y ?? "shared";
23337
- const xResolve = resolveConfig?.scale?.x ?? "shared";
24055
+ const xDefault = facetDirection === "column" ? "independent" : "shared";
24056
+ const yDefault = facetDirection === "row" ? "independent" : "shared";
24057
+ const yResolve = resolveConfig?.scale?.y ?? yDefault;
24058
+ const xResolve = resolveConfig?.scale?.x ?? xDefault;
23338
24059
  const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
23339
24060
  const sharedXDomain = xResolve === "shared" && fullScales.x ? fullScales.x.scale.domain() : void 0;
23340
24061
  const sharedYDomain = yResolve === "shared" && fullScales.y ? fullScales.y.scale.domain() : void 0;
@@ -23399,6 +24120,16 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23399
24120
  tickMarks: false
23400
24121
  };
23401
24122
  }
24123
+ const isBottomRow = gridPanel.row === grid.rows - 1;
24124
+ if (xResolve === "shared" && facetDirection === "row" && !isBottomRow && panelAxes.x) {
24125
+ panelAxes.x = {
24126
+ ...panelAxes.x,
24127
+ ticks: [],
24128
+ label: void 0,
24129
+ domainLine: false,
24130
+ tickMarks: false
24131
+ };
24132
+ }
23402
24133
  const panelMarks = renderer ? renderer(
23403
24134
  panelSpecWithDomains,
23404
24135
  panelScales,
@@ -23452,10 +24183,11 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23452
24183
  annotations: panelAnnotations,
23453
24184
  header: {
23454
24185
  text: gridPanel.key,
23455
- x: gridPanel.headerPos.x,
24186
+ x: facetDirection === "row" ? gridPanel.area.x : gridPanel.headerPos.x,
23456
24187
  y: gridPanel.headerPos.y,
23457
24188
  fontSize: theme.fonts.sizes.body,
23458
- fontWeight: theme.fonts.weights.medium
24189
+ fontWeight: theme.fonts.weights.medium,
24190
+ ...facetDirection === "row" ? { textAnchor: "start" } : {}
23459
24191
  }
23460
24192
  });
23461
24193
  }
@@ -23492,6 +24224,19 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23492
24224
  chartSpec.data
23493
24225
  );
23494
24226
  assignAnimationIndices(allMarks, resolvedAnimation);
24227
+ const facetRenderModeWarnings = [];
24228
+ resolveMarkRenderMode(
24229
+ {
24230
+ requested: options.renderer,
24231
+ markType: chartSpec.markType,
24232
+ pointCount: allMarks.reduce((n, m) => m.type === "point" ? n + 1 : n, 0),
24233
+ display: chartSpec.display,
24234
+ faceted: true,
24235
+ layered: options.layered ?? false
24236
+ },
24237
+ facetRenderModeWarnings
24238
+ );
24239
+ emitSpecWarnings(facetRenderModeWarnings, options.onWarn);
23495
24240
  return {
23496
24241
  area: chartArea,
23497
24242
  chrome: allFootnotes.length > 0 ? { ...dims.chrome, footnotes: allFootnotes } : dims.chrome,
@@ -23508,6 +24253,7 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23508
24253
  a11y: {
23509
24254
  altText,
23510
24255
  dataTableFallback,
24256
+ ...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
23511
24257
  role: "img",
23512
24258
  keyboardNavigable: allMarks.length > 0,
23513
24259
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
@@ -23523,7 +24269,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23523
24269
  panels: panelLayouts,
23524
24270
  facetField: facetChannel.field,
23525
24271
  columns: grid.columns,
23526
- sharedScales: yResolve === "shared" && xResolve === "shared"
24272
+ sharedScales: yResolve === "shared" && xResolve === "shared",
24273
+ direction: facetDirection
23527
24274
  }
23528
24275
  };
23529
24276
  }
@@ -23562,14 +24309,15 @@ function compileSankey2(spec, options) {
23562
24309
  function compileTileMap2(spec, options) {
23563
24310
  return compileTileMap(spec, options);
23564
24311
  }
23565
- function compileMap2(spec, options) {
23566
- return compileMap(spec, options);
24312
+ function compileGeoMap2(spec, options) {
24313
+ return compileGeoMap(spec, options);
23567
24314
  }
23568
24315
  function compileBarList2(spec, options) {
23569
24316
  return compileBarList(spec, options);
23570
24317
  }
23571
24318
  export {
23572
24319
  ARROWHEAD_LENGTH,
24320
+ AUTO_CANVAS_THRESHOLD,
23573
24321
  BOLD_SPAN_FONT_WEIGHT,
23574
24322
  DEFAULT_ANNOTATION_FONT_SIZE,
23575
24323
  DEFAULT_LINE_HEIGHT,
@@ -23577,15 +24325,16 @@ export {
23577
24325
  EXIT_DEFAULTS,
23578
24326
  UPDATE_DEFAULTS,
23579
24327
  buildAreaPath,
24328
+ buildEdgeTooltip,
23580
24329
  buildLinePath,
23581
24330
  clampStaggerDelay,
23582
24331
  clearRenderers,
23583
24332
  compile,
23584
24333
  compileBarList2 as compileBarList,
23585
24334
  compileChart,
24335
+ compileGeoMap2 as compileGeoMap,
23586
24336
  compileGraph2 as compileGraph,
23587
24337
  compileLayer2 as compileLayer,
23588
- compileMap2 as compileMap,
23589
24338
  compileSankey2 as compileSankey,
23590
24339
  compileTable,
23591
24340
  compileTileMap2 as compileTileMap,