@opendata-ai/openchart-engine 8.0.0-rc.8 → 8.0.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 +149 -12
  2. package/dist/index.js +894 -171
  3. package/dist/index.js.map +1 -1
  4. package/package.json +3 -4
  5. package/src/__test-fixtures__/specs.ts +0 -182
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
  7. package/src/__tests__/annotation-behavior.test.ts +0 -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 -626
  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 -1450
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
  139. package/src/compiler/__tests__/animation.test.ts +0 -177
  140. package/src/compiler/__tests__/compile.test.ts +0 -87
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
  160. package/src/compiler/__tests__/keys.test.ts +0 -83
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
  162. package/src/compiler/__tests__/normalize.test.ts +0 -399
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -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 -1004
  185. package/src/geo/compile-map.ts +0 -1341
  186. package/src/geo/join.ts +0 -115
  187. package/src/geo/projections.ts +0 -45
  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 -747
  215. package/src/legend/continuous.ts +0 -327
  216. package/src/legend/size.ts +0 -407
  217. package/src/legend/suppression.ts +0 -226
  218. package/src/legend/wrap.ts +0 -117
  219. package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
  220. package/src/sankey/__tests__/layout.test.ts +0 -165
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
  222. package/src/sankey/__tests__/node-sort.test.ts +0 -45
  223. package/src/sankey/compile-sankey.ts +0 -706
  224. package/src/sankey/layout.ts +0 -184
  225. package/src/sankey/types.ts +0 -41
  226. package/src/tables/__tests__/bar-column.test.ts +0 -147
  227. package/src/tables/__tests__/category-colors.test.ts +0 -230
  228. package/src/tables/__tests__/compile-table.test.ts +0 -236
  229. package/src/tables/__tests__/format-cells.test.ts +0 -143
  230. package/src/tables/__tests__/heatmap.test.ts +0 -148
  231. package/src/tables/__tests__/pagination.test.ts +0 -78
  232. package/src/tables/__tests__/search.test.ts +0 -94
  233. package/src/tables/__tests__/sort.test.ts +0 -107
  234. package/src/tables/__tests__/sparkline.test.ts +0 -122
  235. package/src/tables/bar-column.ts +0 -94
  236. package/src/tables/category-colors.ts +0 -78
  237. package/src/tables/compile-table.ts +0 -427
  238. package/src/tables/format-cells.ts +0 -115
  239. package/src/tables/heatmap.ts +0 -141
  240. package/src/tables/pagination.ts +0 -46
  241. package/src/tables/search.ts +0 -66
  242. package/src/tables/sort.ts +0 -69
  243. package/src/tables/sparkline.ts +0 -113
  244. package/src/tables/utils.ts +0 -16
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
  246. package/src/tilemap/compile-tilemap.ts +0 -755
  247. package/src/tilemap/layout.ts +0 -172
  248. package/src/tilemap/types.ts +0 -33
  249. package/src/tooltips/__tests__/compute.test.ts +0 -582
  250. package/src/tooltips/compute.ts +0 -700
  251. package/src/transforms/__tests__/aggregate.test.ts +0 -159
  252. package/src/transforms/__tests__/bin.test.ts +0 -88
  253. package/src/transforms/__tests__/calculate.test.ts +0 -146
  254. package/src/transforms/__tests__/conditional.test.ts +0 -127
  255. package/src/transforms/__tests__/filter-relative.test.ts +0 -202
  256. package/src/transforms/__tests__/filter.test.ts +0 -59
  257. package/src/transforms/__tests__/fold.test.ts +0 -79
  258. package/src/transforms/__tests__/index.test.ts +0 -93
  259. package/src/transforms/__tests__/predicates.test.ts +0 -193
  260. package/src/transforms/__tests__/timeunit.test.ts +0 -129
  261. package/src/transforms/__tests__/window.test.ts +0 -286
  262. package/src/transforms/aggregate.ts +0 -131
  263. package/src/transforms/bin.ts +0 -87
  264. package/src/transforms/calculate.ts +0 -60
  265. package/src/transforms/conditional.ts +0 -46
  266. package/src/transforms/filter.ts +0 -122
  267. package/src/transforms/fold.ts +0 -49
  268. package/src/transforms/index.ts +0 -60
  269. package/src/transforms/predicates.ts +0 -123
  270. package/src/transforms/timeunit.ts +0 -88
  271. package/src/transforms/window.ts +0 -185
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getHeightClass,
10
10
  getLayoutStrategy,
11
11
  isAxislessMark as isAxislessMark3,
12
+ MAX_SR_TABLE_ROWS,
12
13
  resolveTheme as resolveTheme7
13
14
  } from "@opendata-ai/openchart-core";
14
15
 
@@ -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),
@@ -10547,15 +10596,10 @@ function normalizeSankeySpec(spec, _warnings) {
10547
10596
  };
10548
10597
  }
10549
10598
  function normalizeGraphSpec(spec) {
10550
- const defaultLayout = {
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
+ };
10559
10603
  return {
10560
10604
  type: "graph",
10561
10605
  nodes: spec.nodes,
@@ -10567,7 +10611,10 @@ function normalizeGraphSpec(spec) {
10567
10611
  annotations: normalizeAnnotations(spec.annotations),
10568
10612
  theme: spec.theme ?? {},
10569
10613
  darkMode: spec.darkMode ?? "off",
10570
- watermark: spec.watermark ?? true
10614
+ watermark: spec.watermark ?? true,
10615
+ animation: spec.animation,
10616
+ interaction: spec.interaction,
10617
+ legend: spec.legend
10571
10618
  };
10572
10619
  }
10573
10620
  function normalizeTileMapSpec(spec, warnings) {
@@ -10623,7 +10670,7 @@ function normalizeTileMapSpec(spec, warnings) {
10623
10670
  valueFormat: spec.valueFormat
10624
10671
  };
10625
10672
  }
10626
- function normalizeMapSpec(spec, _warnings) {
10673
+ function normalizeGeoMapSpec(spec, _warnings) {
10627
10674
  return {
10628
10675
  type: "map",
10629
10676
  geo: {
@@ -10683,8 +10730,8 @@ function normalizeSpec(spec, warnings = []) {
10683
10730
  if (isTileMapSpec(spec)) {
10684
10731
  return normalizeTileMapSpec(spec, warnings);
10685
10732
  }
10686
- if (isMapSpec(spec)) {
10687
- return normalizeMapSpec(spec, warnings);
10733
+ if (isGeoMapSpec(spec)) {
10734
+ return normalizeGeoMapSpec(spec, warnings);
10688
10735
  }
10689
10736
  if (isBarListSpec(spec)) {
10690
10737
  return normalizeBarListSpec(spec, warnings);
@@ -11000,6 +11047,40 @@ function validateCalendarSpec(spec, errors) {
11000
11047
  seenDays.set(dayKey, i);
11001
11048
  }
11002
11049
  }
11050
+ var MAX_PLAUSIBLE_POINT_RADIUS = 50;
11051
+ function pointSizeError(path2, value2) {
11052
+ const radius = Math.max(1, Math.round(Math.sqrt(value2 / Math.PI)));
11053
+ return {
11054
+ 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.`,
11055
+ path: path2,
11056
+ code: "INVALID_VALUE",
11057
+ 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)).`
11058
+ };
11059
+ }
11060
+ function validatePointSize(spec, errors) {
11061
+ const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
11062
+ if (markType !== "point") return;
11063
+ if (spec.mark && typeof spec.mark === "object") {
11064
+ const size = spec.mark.size;
11065
+ if (typeof size === "number" && size > MAX_PLAUSIBLE_POINT_RADIUS) {
11066
+ errors.push(pointSizeError("mark.size", size));
11067
+ }
11068
+ }
11069
+ const sizeEnc = spec.encoding && typeof spec.encoding === "object" ? spec.encoding.size : void 0;
11070
+ if (!sizeEnc || typeof sizeEnc !== "object") return;
11071
+ if (typeof sizeEnc.value === "number" && sizeEnc.value > MAX_PLAUSIBLE_POINT_RADIUS) {
11072
+ errors.push(pointSizeError("encoding.size.value", sizeEnc.value));
11073
+ }
11074
+ const range3 = sizeEnc.scale && typeof sizeEnc.scale === "object" ? sizeEnc.scale.range : void 0;
11075
+ if (Array.isArray(range3)) {
11076
+ const worst = range3.find(
11077
+ (entry) => typeof entry === "number" && entry > MAX_PLAUSIBLE_POINT_RADIUS
11078
+ );
11079
+ if (typeof worst === "number") {
11080
+ errors.push(pointSizeError("encoding.size.scale.range", worst));
11081
+ }
11082
+ }
11083
+ }
11003
11084
  function validateChartSpec(spec, errors) {
11004
11085
  const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
11005
11086
  if (!Array.isArray(spec.data)) {
@@ -11105,6 +11186,7 @@ function validateChartSpec(spec, errors) {
11105
11186
  if (markType === "calendar") {
11106
11187
  validateCalendarSpec(spec, errors);
11107
11188
  }
11189
+ validatePointSize(spec, errors);
11108
11190
  if (Array.isArray(spec.transform)) {
11109
11191
  const transforms = spec.transform;
11110
11192
  for (let i = 0; i < transforms.length; i++) {
@@ -11317,6 +11399,52 @@ function validateChartSpec(spec, errors) {
11317
11399
  }
11318
11400
  }
11319
11401
  }
11402
+ for (const channel of ["row", "column"]) {
11403
+ const ch = encoding[channel];
11404
+ if (ch && typeof ch === "object") {
11405
+ const facetCh = ch;
11406
+ if (!facetCh.field || typeof facetCh.field !== "string") {
11407
+ errors.push({
11408
+ message: `Spec error: encoding.${channel}.field is required and must be a non-empty string`,
11409
+ path: `encoding.${channel}.field`,
11410
+ code: "MISSING_FIELD",
11411
+ suggestion: `Add a field property to encoding.${channel} (e.g. { field: "category", type: "nominal" })`
11412
+ });
11413
+ } else if (!dataColumns.has(facetCh.field) && !transformFields.has(facetCh.field)) {
11414
+ errors.push({
11415
+ message: `Spec error: encoding.${channel}.field "${facetCh.field}" does not exist in data. Available columns: ${availableColumns}`,
11416
+ path: `encoding.${channel}.field`,
11417
+ code: "DATA_FIELD_MISSING",
11418
+ suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(facetCh.field, columnList)}`
11419
+ });
11420
+ }
11421
+ if (!facetCh.type || facetCh.type !== "nominal" && facetCh.type !== "ordinal") {
11422
+ errors.push({
11423
+ message: `Spec error: encoding.${channel}.type must be "nominal" or "ordinal"${facetCh.type ? `, got "${facetCh.type}"` : ""}`,
11424
+ path: `encoding.${channel}.type`,
11425
+ code: facetCh.type ? "INVALID_VALUE" : "MISSING_FIELD",
11426
+ suggestion: `Set encoding.${channel}.type to "nominal" or "ordinal"`
11427
+ });
11428
+ }
11429
+ if (encoding.facet && typeof encoding.facet === "object") {
11430
+ errors.push({
11431
+ message: `Spec error: encoding.${channel} and encoding.facet cannot be used together`,
11432
+ path: `encoding.${channel}`,
11433
+ code: "INVALID_VALUE",
11434
+ suggestion: `Use either encoding.${channel} or encoding.facet, not both`
11435
+ });
11436
+ }
11437
+ const otherChannel = channel === "row" ? "column" : "row";
11438
+ if (encoding[otherChannel]) {
11439
+ errors.push({
11440
+ message: "Spec error: encoding.row and encoding.column cannot be used together (cross-product faceting is not yet supported)",
11441
+ path: `encoding.${channel}`,
11442
+ code: "INVALID_VALUE",
11443
+ suggestion: "Use encoding.facet with columns for grid layout, or use only row or column"
11444
+ });
11445
+ }
11446
+ }
11447
+ }
11320
11448
  if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
11321
11449
  errors.push({
11322
11450
  message: `Spec error: darkMode must be "auto", "force", or "off"`,
@@ -11550,14 +11678,28 @@ function validateGraphSpec(spec, errors) {
11550
11678
  }
11551
11679
  if (spec.encoding && typeof spec.encoding === "object") {
11552
11680
  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"];
11681
+ const nodeFields = /* @__PURE__ */ new Set();
11682
+ for (const node of nodes) {
11683
+ if (node && typeof node === "object") {
11684
+ for (const key of Object.keys(node)) nodeFields.add(key);
11685
+ }
11686
+ }
11687
+ const edgeFields = /* @__PURE__ */ new Set();
11688
+ for (const edge of edges) {
11689
+ if (edge && typeof edge === "object") {
11690
+ for (const key of Object.keys(edge)) edgeFields.add(key);
11691
+ }
11692
+ }
11693
+ const nodeChannels = [
11694
+ "nodeColor",
11695
+ "nodeSize",
11696
+ "nodeOpacity",
11697
+ "nodeLabel",
11698
+ "nodeLabelPriority"
11699
+ ];
11558
11700
  for (const channel of nodeChannels) {
11559
11701
  const ch = encoding[channel];
11560
- if (ch?.field && typeof ch.field === "string" && !nodeFields.has(ch.field)) {
11702
+ if (ch?.field && typeof ch.field === "string" && nodes.length > 0 && !nodeFields.has(ch.field)) {
11561
11703
  errors.push({
11562
11704
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
11563
11705
  path: `encoding.${channel}.field`,
@@ -11566,10 +11708,10 @@ function validateGraphSpec(spec, errors) {
11566
11708
  });
11567
11709
  }
11568
11710
  }
11569
- const edgeChannels = ["edgeColor", "edgeWidth"];
11711
+ const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
11570
11712
  for (const channel of edgeChannels) {
11571
11713
  const ch = encoding[channel];
11572
- if (ch?.field && typeof ch.field === "string" && firstEdge && !edgeFields.has(ch.field)) {
11714
+ if (ch?.field && typeof ch.field === "string" && edges.length > 0 && !edgeFields.has(ch.field)) {
11573
11715
  errors.push({
11574
11716
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
11575
11717
  path: `encoding.${channel}.field`,
@@ -11782,7 +11924,7 @@ function validateTileMapSpec(spec, errors) {
11782
11924
  });
11783
11925
  }
11784
11926
  }
11785
- function validateMapSpec(spec, errors) {
11927
+ function validateGeoMapSpec(spec, errors) {
11786
11928
  if (!spec.geo || typeof spec.geo !== "object") {
11787
11929
  errors.push({
11788
11930
  message: 'Spec error: map spec requires a "geo" object with a TopoJSON "features" field',
@@ -12162,6 +12304,22 @@ function validateLayerSpec(spec, errors) {
12162
12304
  }
12163
12305
  }
12164
12306
  }
12307
+ function validateAnimation(spec, errors) {
12308
+ const animation = spec.animation;
12309
+ if (!animation || typeof animation !== "object" || Array.isArray(animation)) return;
12310
+ const update = animation.update;
12311
+ if (!update || typeof update !== "object" || Array.isArray(update)) return;
12312
+ const maxMarks = update.maxMarks;
12313
+ if (maxMarks === void 0) return;
12314
+ if (typeof maxMarks !== "number" || !Number.isFinite(maxMarks) || maxMarks < 1) {
12315
+ errors.push({
12316
+ message: `Spec error: animation.update.maxMarks must be a number >= 1, got ${JSON.stringify(maxMarks)}`,
12317
+ path: "animation.update.maxMarks",
12318
+ code: "INVALID_VALUE",
12319
+ 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."
12320
+ });
12321
+ }
12322
+ }
12165
12323
  function validateSpec(spec) {
12166
12324
  const errors = [];
12167
12325
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
@@ -12266,10 +12424,13 @@ function validateSpec(spec) {
12266
12424
  } else if (isTileMap) {
12267
12425
  validateTileMapSpec(obj, errors);
12268
12426
  } else if (isMap) {
12269
- validateMapSpec(obj, errors);
12427
+ validateGeoMapSpec(obj, errors);
12270
12428
  } else if (isBarList) {
12271
12429
  validateBarListSpec(obj, errors);
12272
12430
  }
12431
+ if (!isGraph) {
12432
+ validateAnimation(obj, errors);
12433
+ }
12273
12434
  if (errors.length > 0) {
12274
12435
  return { valid: false, errors, normalized: null };
12275
12436
  }
@@ -12293,6 +12454,36 @@ ${errorMessages}`);
12293
12454
  return { spec: normalized, warnings };
12294
12455
  }
12295
12456
 
12457
+ // src/layout/shared.ts
12458
+ function chromeToInput(chrome) {
12459
+ return {
12460
+ eyebrow: chrome.eyebrow,
12461
+ title: chrome.title,
12462
+ subtitle: chrome.subtitle,
12463
+ source: chrome.source,
12464
+ byline: chrome.byline,
12465
+ footer: chrome.footer,
12466
+ brand: chrome.brand
12467
+ };
12468
+ }
12469
+ function scalePadding(basePadding, width, height) {
12470
+ const minDim = Math.min(width, height);
12471
+ if (minDim >= 500) return basePadding;
12472
+ if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
12473
+ const t = (minDim - 200) / 300;
12474
+ return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
12475
+ }
12476
+ function bottomMargin(bottomHeight, padding, xAxisHeight) {
12477
+ return padding + bottomHeight + xAxisHeight;
12478
+ }
12479
+ var INLINE_TICK_OVERHANG_PAD = 6;
12480
+ function resolveChromeLayout(spec, options) {
12481
+ const encoding = spec?.encoding;
12482
+ const isFaceted = !!(encoding?.facet || encoding?.row || encoding?.column);
12483
+ if (isFaceted) return "subtract";
12484
+ return spec?.chromeLayout ?? options?.chromeLayout ?? "subtract";
12485
+ }
12486
+
12296
12487
  // src/barlist/compile-barlist.ts
12297
12488
  var DEFAULT_ROW_GAP = 8;
12298
12489
  var LABEL_BAR_GAP = 12;
@@ -12340,14 +12531,19 @@ function compileBarList(spec, options) {
12340
12531
  watermark
12341
12532
  );
12342
12533
  const padding = theme.spacing.padding;
12534
+ const chromeLayout = resolveChromeLayout(
12535
+ spec,
12536
+ options
12537
+ );
12538
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
12343
12539
  const fullArea = {
12344
12540
  x: padding,
12345
12541
  y: padding + chrome.topHeight,
12346
12542
  width: options.width - padding * 2,
12347
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12543
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12348
12544
  };
12349
12545
  if (fullArea.width <= 0 || fullArea.height <= 0) {
12350
- return emptyLayout2(chrome, theme, options, watermark);
12546
+ return emptyLayout2(chrome, theme, { ...options, height: grownHeight }, watermark);
12351
12547
  }
12352
12548
  const labelField = barlistSpec.encoding.label.field;
12353
12549
  const valueField = barlistSpec.encoding.value.field;
@@ -12532,7 +12728,7 @@ function compileBarList(spec, options) {
12532
12728
  a11y,
12533
12729
  theme,
12534
12730
  width: options.width,
12535
- height: options.height,
12731
+ height: grownHeight,
12536
12732
  animation: resolvedAnimation,
12537
12733
  watermark,
12538
12734
  measureText
@@ -13660,6 +13856,7 @@ var LEGEND_PADDING = 8;
13660
13856
  var LEGEND_RIGHT_WIDTH = 120;
13661
13857
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
13662
13858
  var TOP_LEGEND_MAX_ROWS = 2;
13859
+ var ARC_LABEL_CROWDING_LIMIT = 21;
13663
13860
  function hasLegendContent(content) {
13664
13861
  return content.entries.length > 0 || content.continuous != null;
13665
13862
  }
@@ -13817,6 +14014,12 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
13817
14014
  }
13818
14015
  }
13819
14016
  }
14017
+ const density = spec.labels.density;
14018
+ const everySliceLabeled = density === "all" || density === "auto";
14019
+ const uncrowded = seriesCount > 0 && seriesCount <= ARC_LABEL_CROWDING_LIMIT;
14020
+ if (spec.markType === "arc" && everySliceLabeled && uncrowded && !spec.userExplicit?.legend) {
14021
+ entries = [];
14022
+ }
13820
14023
  const labelStyle = {
13821
14024
  fontFamily: theme.fonts.family,
13822
14025
  fontSize: theme.fonts.sizes.small,
@@ -13824,7 +14027,8 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
13824
14027
  fill: theme.colors.text,
13825
14028
  lineHeight: 1.3
13826
14029
  };
13827
- const resolvedPosition = spec.legend?.position ?? (strategy.legendPosition === "right" ? "right" : "top");
14030
+ const specPosition = spec.legend?.position === "top-left" ? "top" : spec.legend?.position;
14031
+ const resolvedPosition = specPosition ?? (strategy.legendPosition === "right" ? "right" : "top");
13828
14032
  if (entries.length === 0) {
13829
14033
  return emptyContent(resolvedPosition, theme);
13830
14034
  }
@@ -14825,30 +15029,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
14825
15029
  return result;
14826
15030
  }
14827
15031
 
14828
- // src/layout/shared.ts
14829
- function chromeToInput(chrome) {
14830
- return {
14831
- eyebrow: chrome.eyebrow,
14832
- title: chrome.title,
14833
- subtitle: chrome.subtitle,
14834
- source: chrome.source,
14835
- byline: chrome.byline,
14836
- footer: chrome.footer,
14837
- brand: chrome.brand
14838
- };
14839
- }
14840
- function scalePadding(basePadding, width, height) {
14841
- const minDim = Math.min(width, height);
14842
- if (minDim >= 500) return basePadding;
14843
- if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
14844
- const t = (minDim - 200) / 300;
14845
- return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
14846
- }
14847
- function bottomMargin(bottomHeight, padding, xAxisHeight) {
14848
- return padding + bottomHeight + xAxisHeight;
14849
- }
14850
- var INLINE_TICK_OVERHANG_PAD = 6;
14851
-
14852
15032
  // src/layout/plan.ts
14853
15033
  function createMeasureFn(measureText) {
14854
15034
  if (measureText) {
@@ -15070,7 +15250,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
15070
15250
  if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
15071
15251
  topMargin += effectiveTopAxisGap;
15072
15252
  const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
15073
- const chartHeight = Math.max(0, height - topMargin - bMargin);
15253
+ const chromeLayout = resolveChromeLayout(renderSpec, options);
15254
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
15255
+ const chartHeight = Math.max(0, effectiveHeight - topMargin - bMargin);
15074
15256
  const rightMargin = hPad + (isRadial ? hPad : axisMargin);
15075
15257
  const rightAxisReserve = options.rightAxisReserve ?? 0;
15076
15258
  const provisionalArea = {
@@ -15222,11 +15404,12 @@ function compileLayer(spec, options, compileChart2) {
15222
15404
  const singleSpec = buildPrimarySpec(leaves, spec);
15223
15405
  return compileChart2(singleSpec, options);
15224
15406
  }
15407
+ const layerOptions = { ...options, layered: true };
15225
15408
  if (spec.resolve?.scale?.y === "independent") {
15226
- return compileLayerIndependent(leaves, spec, options, compileChart2);
15409
+ return compileLayerIndependent(leaves, spec, layerOptions, compileChart2);
15227
15410
  }
15228
15411
  const primarySpec = buildPrimarySpec(leaves, spec);
15229
- const primaryLayout = compileChart2(primarySpec, options);
15412
+ const primaryLayout = compileChart2(primarySpec, layerOptions);
15230
15413
  const allMarks = [];
15231
15414
  const seenLabels = /* @__PURE__ */ new Set();
15232
15415
  const pLegend = primaryLayout.legend;
@@ -15239,7 +15422,7 @@ function compileLayer(spec, options, compileChart2) {
15239
15422
  zIndex: leaf.zIndex ?? i
15240
15423
  }));
15241
15424
  indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
15242
- const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
15425
+ const leafOptions = { ...layerOptions, frozenChartArea: primaryLayout.area };
15243
15426
  const sharedDomains = computeSharedDomains(leaves);
15244
15427
  for (const { leaf } of indexedLeaves) {
15245
15428
  const themedLeaf = {
@@ -15348,11 +15531,9 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
15348
15531
  };
15349
15532
  const xField0 = leaf0.encoding?.x?.field;
15350
15533
  const xField1 = leaf1.encoding?.x?.field;
15351
- const unionXValues = /* @__PURE__ */ new Set();
15352
- if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
15353
- if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
15354
- let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
15355
- let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
15534
+ const union = withUnionXDomain(leaf0, leaf1);
15535
+ let leaf0WithUnionX = union.leaf0;
15536
+ let leaf1WithUnionX = union.leaf1;
15356
15537
  const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
15357
15538
  if (aligned) {
15358
15539
  leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
@@ -15499,20 +15680,75 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
15499
15680
  tooltipDescriptors: mergedTooltips
15500
15681
  };
15501
15682
  }
15502
- function ensureXDomainCoverage(leaf, xField, allXValues) {
15503
- if (!xField || allXValues.size === 0) return leaf;
15504
- const existingXValues = /* @__PURE__ */ new Set();
15505
- for (const row of leaf.data) existingXValues.add(row[xField]);
15506
- const missingRows = [];
15507
- for (const xVal of allXValues) {
15508
- if (!existingXValues.has(xVal)) {
15509
- missingRows.push({ [xField]: xVal });
15683
+ function withXDomain(leaf, domain) {
15684
+ if (!leaf.encoding?.x) return leaf;
15685
+ return {
15686
+ ...leaf,
15687
+ encoding: {
15688
+ ...leaf.encoding,
15689
+ x: {
15690
+ ...leaf.encoding.x,
15691
+ scale: {
15692
+ ...leaf.encoding.x.scale,
15693
+ domain
15694
+ }
15695
+ }
15696
+ }
15697
+ };
15698
+ }
15699
+ function withUnionXDomain(leaf0, leaf1) {
15700
+ const xEnc0 = leaf0.encoding?.x;
15701
+ const xEnc1 = leaf1.encoding?.x;
15702
+ const xField0 = xEnc0?.field;
15703
+ const xField1 = xEnc1?.field;
15704
+ if (!xField0 || !xField1) return { leaf0, leaf1 };
15705
+ const xType = xEnc0?.type ?? xEnc1?.type;
15706
+ const pinned0 = xEnc0?.scale?.domain !== void 0;
15707
+ const pinned1 = xEnc1?.scale?.domain !== void 0;
15708
+ if (pinned0 && pinned1) return { leaf0, leaf1 };
15709
+ const values0 = leaf0.data.map((row) => row[xField0]);
15710
+ const values1 = leaf1.data.map((row) => row[xField1]);
15711
+ const allValues = [...values0, ...values1].filter((v) => v != null);
15712
+ if (allValues.length === 0) return { leaf0, leaf1 };
15713
+ let domain;
15714
+ if (xType === "temporal") {
15715
+ let minMs = Number.POSITIVE_INFINITY;
15716
+ let maxMs = Number.NEGATIVE_INFINITY;
15717
+ for (const v of allValues) {
15718
+ const ms = (v instanceof Date ? v : new Date(String(v))).getTime();
15719
+ if (Number.isNaN(ms)) continue;
15720
+ if (ms < minMs) minMs = ms;
15721
+ if (ms > maxMs) maxMs = ms;
15722
+ }
15723
+ if (minMs <= maxMs) {
15724
+ domain = [new Date(minMs).toISOString(), new Date(maxMs).toISOString()];
15725
+ }
15726
+ } else if (xType === "quantitative") {
15727
+ let min4 = Number.POSITIVE_INFINITY;
15728
+ let max4 = Number.NEGATIVE_INFINITY;
15729
+ for (const v of allValues) {
15730
+ const n = typeof v === "number" ? v : Number(v);
15731
+ if (!Number.isFinite(n)) continue;
15732
+ if (n < min4) min4 = n;
15733
+ if (n > max4) max4 = n;
15734
+ }
15735
+ if (min4 <= max4) domain = [min4, max4];
15736
+ } else {
15737
+ const seen = /* @__PURE__ */ new Set();
15738
+ const categories = [];
15739
+ for (const v of allValues) {
15740
+ const s = String(v);
15741
+ if (!seen.has(s)) {
15742
+ seen.add(s);
15743
+ categories.push(s);
15744
+ }
15510
15745
  }
15746
+ if (categories.length > 0) domain = categories;
15511
15747
  }
15512
- if (missingRows.length === 0) return leaf;
15748
+ if (domain === void 0) return { leaf0, leaf1 };
15513
15749
  return {
15514
- ...leaf,
15515
- data: [...leaf.data, ...missingRows]
15750
+ leaf0: pinned0 ? leaf0 : withXDomain(leaf0, domain),
15751
+ leaf1: pinned1 ? leaf1 : withXDomain(leaf1, domain)
15516
15752
  };
15517
15753
  }
15518
15754
  function alignYDomains(leaf0, leaf1) {
@@ -15631,6 +15867,10 @@ function buildPrimarySpec(leaves, layerSpec) {
15631
15867
  ...leaves[0],
15632
15868
  data: allData,
15633
15869
  chrome: layerSpec.chrome ?? leaves[0].chrome,
15870
+ // Annotations come from the parent LayerSpec when present, otherwise
15871
+ // the first flattened leaf's. Later leaves' annotations are silently
15872
+ // dropped. The dual-axis path (compileLayerIndependent) explicitly
15873
+ // clears the secondary axis spec's annotations.
15634
15874
  annotations: layerSpec.annotations ?? leaves[0].annotations,
15635
15875
  labels: layerSpec.labels ?? leaves[0].labels,
15636
15876
  legend: layerSpec.legend ?? leaves[0].legend,
@@ -15731,7 +15971,7 @@ function resolveYouDrawIt(config, marks, scales, chartArea, xField) {
15731
15971
  }
15732
15972
 
15733
15973
  // src/compiler/a11y-warnings.ts
15734
- import { contrastRatio, findAccessibleColor, isOpaqueColor } from "@opendata-ai/openchart-core";
15974
+ import { contrastRatio, findAccessibleColor, isOpaqueColor as isOpaqueColor2 } from "@opendata-ai/openchart-core";
15735
15975
  var GRAPHIC_MIN = 3;
15736
15976
  var TEXT_MIN = 4.5;
15737
15977
  var ADJACENT_FILL_MARKS = /* @__PURE__ */ new Set(["bar", "area", "arc", "waffle"]);
@@ -15739,7 +15979,7 @@ var fmt = (ratio) => `${ratio.toFixed(2)}:1`;
15739
15979
  function collectContrastWarnings(scales, markType, theme) {
15740
15980
  const warnings = [];
15741
15981
  const bg = theme.colors.background;
15742
- const bgIsCheckable = isOpaqueColor(bg);
15982
+ const bgIsCheckable = isOpaqueColor2(bg);
15743
15983
  const series = [];
15744
15984
  if (scales.color && scales.color.type !== "sequential") {
15745
15985
  const ordinal2 = scales.color.scale;
@@ -15787,6 +16027,37 @@ function collectContrastWarnings(scales, markType, theme) {
15787
16027
  return warnings;
15788
16028
  }
15789
16029
 
16030
+ // src/compiler/mark-render-mode.ts
16031
+ var AUTO_CANVAS_THRESHOLD = 1e3;
16032
+ var AUTO_CANVAS_REFUSAL_WARN_THRESHOLD = 5e3;
16033
+ function resolveMarkRenderMode(args, warnings = []) {
16034
+ const { markType, pointCount, display, faceted, layered } = args;
16035
+ const requested = args.requested ?? "auto";
16036
+ if (requested === "svg") return "svg";
16037
+ const refusal = describeRefusal(markType, display, faceted, layered);
16038
+ if (refusal) {
16039
+ if (requested === "canvas") {
16040
+ warnings.push(
16041
+ `Chart warning: renderer "canvas" is not supported ${refusal}; rendering marks as SVG instead.`
16042
+ );
16043
+ } else if (pointCount > AUTO_CANVAS_REFUSAL_WARN_THRESHOLD) {
16044
+ warnings.push(
16045
+ `[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.`
16046
+ );
16047
+ }
16048
+ return "svg";
16049
+ }
16050
+ if (requested === "canvas") return "canvas";
16051
+ return pointCount > AUTO_CANVAS_THRESHOLD ? "canvas" : "svg";
16052
+ }
16053
+ function describeRefusal(markType, display, faceted, layered) {
16054
+ if (markType !== "point") return `for ${markType} marks (point marks only)`;
16055
+ if (faceted) return "on faceted charts";
16056
+ if (layered) return "on layered charts";
16057
+ if (display === "sparkline") return "on sparklines";
16058
+ return void 0;
16059
+ }
16060
+
15790
16061
  // src/compiler/sparkline-defaults.ts
15791
16062
  var TREND_DEADBAND = 5e-4;
15792
16063
  function computeTrend(values) {
@@ -15853,7 +16124,7 @@ function buildSparklineAreaGradient(baseColor) {
15853
16124
  };
15854
16125
  }
15855
16126
 
15856
- // src/geo/compile-map.ts
16127
+ // src/geo/compile-geo-map.ts
15857
16128
  import {
15858
16129
  adaptTheme as adaptTheme2,
15859
16130
  buildD3Formatter as buildD3Formatter2,
@@ -17754,7 +18025,7 @@ function identity_default4() {
17754
18025
  return projection2;
17755
18026
  }
17756
18027
 
17757
- // src/geo/compile-map.ts
18028
+ // src/geo/compile-geo-map.ts
17758
18029
  import { feature as topoFeature, mesh as topoMesh } from "topojson-client";
17759
18030
 
17760
18031
  // src/geo/join.ts
@@ -17843,13 +18114,32 @@ function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
17843
18114
 
17844
18115
  // src/geo/projections.ts
17845
18116
  var MERCATOR_FIT_MIN_LAT = -60;
17846
- function createProjection(type, width, height, geojson) {
18117
+ function createProjection(type, width, height, geojson, inset = 0) {
17847
18118
  if (type === "identity") {
18119
+ if (inset > 0) {
18120
+ return identity_default4().fitExtent(
18121
+ [
18122
+ [inset, inset],
18123
+ [width - inset, height - inset]
18124
+ ],
18125
+ geojson
18126
+ );
18127
+ }
17848
18128
  return identity_default4().fitSize([width, height], geojson);
17849
18129
  }
17850
18130
  const projection2 = type === "albersUsa" ? albersUsa_default() : type === "equalEarth" ? equalEarth_default() : mercator_default();
17851
18131
  const fitGeometry = type === "mercator" ? excludePolarFeatures(geojson) : geojson;
17852
- projection2.fitSize([width, height], fitGeometry);
18132
+ if (inset > 0) {
18133
+ projection2.fitExtent(
18134
+ [
18135
+ [inset, inset],
18136
+ [width - inset, height - inset]
18137
+ ],
18138
+ fitGeometry
18139
+ );
18140
+ } else {
18141
+ projection2.fitSize([width, height], fitGeometry);
18142
+ }
17853
18143
  return projection2;
17854
18144
  }
17855
18145
  function excludePolarFeatures(geojson) {
@@ -17868,7 +18158,8 @@ function excludePolarFeatures(geojson) {
17868
18158
  return { type: "FeatureCollection", features: filtered };
17869
18159
  }
17870
18160
 
17871
- // src/geo/compile-map.ts
18161
+ // src/geo/compile-geo-map.ts
18162
+ var OVERLAY_LEGEND_INSET = 18;
17872
18163
  function validateGeoFeatures(geo) {
17873
18164
  if (!geo.features) {
17874
18165
  throw new Error(
@@ -17889,14 +18180,14 @@ function validateGeoFeatures(geo) {
17889
18180
  }
17890
18181
  return topo;
17891
18182
  }
17892
- function compileMap(spec, options) {
18183
+ function compileGeoMap(spec, options) {
17893
18184
  const sugarWarnings = [];
17894
18185
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
17895
18186
  const { spec: normalized, warnings } = compile(expandedSpec);
17896
18187
  emitSpecWarnings([...sugarWarnings, ...warnings], options.onWarn);
17897
18188
  if (!("type" in normalized) || normalized.type !== "map") {
17898
18189
  throw new Error(
17899
- "compileMap received a non-map spec. Use compileChart, compileTileMap, compileSankey, or compileBarList instead."
18190
+ "compileGeoMap received a non-map spec. Use compileChart, compileTileMap, compileSankey, or compileBarList instead."
17900
18191
  );
17901
18192
  }
17902
18193
  const mapSpec = normalized;
@@ -17926,11 +18217,15 @@ function compileMap(spec, options) {
17926
18217
  watermark
17927
18218
  );
17928
18219
  const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
18220
+ const chromeLayout = resolveChromeLayout(
18221
+ spec,
18222
+ options
18223
+ );
17929
18224
  const fullArea = {
17930
18225
  x: padding,
17931
18226
  y: padding + chrome.topHeight,
17932
18227
  width: options.width - padding * 2,
17933
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
18228
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
17934
18229
  };
17935
18230
  if (fullArea.width <= 0 || fullArea.height <= 0) {
17936
18231
  return emptyLayout3(chrome, theme, options, watermark);
@@ -17941,11 +18236,13 @@ function compileMap(spec, options) {
17941
18236
  const compileWarnings = [];
17942
18237
  const showLegend = mapSpec.legend?.show !== false;
17943
18238
  const colorEncoding = mapSpec.encoding.color;
18239
+ const showChoroplethLegend = showLegend && colorEncoding?.legend !== null;
17944
18240
  const isQuantitative = colorEncoding?.type === "quantitative";
17945
18241
  const legendPosition = mapSpec.legend?.position === "bottom" ? "bottom" : "top";
18242
+ const pointLegendOverlay = mapSpec.legend?.position === "top-left";
17946
18243
  let legendBlockHeight = 0;
17947
18244
  let continuousContent = null;
17948
- if (showLegend && colorEncoding && isQuantitative) {
18245
+ if (showChoroplethLegend && colorEncoding && isQuantitative) {
17949
18246
  const colorValues = [];
17950
18247
  for (const row of mapSpec.data) {
17951
18248
  const v = row[colorEncoding.field];
@@ -17968,11 +18265,11 @@ function compileMap(spec, options) {
17968
18265
  const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
17969
18266
  legendBlockHeight = continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
17970
18267
  }
17971
- } else if (showLegend && colorEncoding) {
18268
+ } else if (showChoroplethLegend && colorEncoding) {
17972
18269
  const labelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
17973
18270
  legendBlockHeight = Math.max(10, labelHeight) + 6;
17974
18271
  }
17975
- if (showLegend && mapSpec.points?.color) {
18272
+ if (showLegend && !pointLegendOverlay && mapSpec.points?.color) {
17976
18273
  if (mapSpec.points.color.type === "quantitative") {
17977
18274
  const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
17978
18275
  legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
@@ -17987,7 +18284,18 @@ function compileMap(spec, options) {
17987
18284
  return emptyLayout3(chrome, theme, options, watermark);
17988
18285
  }
17989
18286
  const projectionType = mapSpec.geo.projection;
17990
- const projection2 = createProjection(projectionType, fullArea.width, mapAreaHeight, geoCollection);
18287
+ let pointInset = 0;
18288
+ if (mapSpec.points) {
18289
+ const sizeRange = mapSpec.points.size?.scale?.range;
18290
+ pointInset = sizeRange ? sizeRange[1] : SIZE_SCALE_DEFAULTS.mapPoint.range[1];
18291
+ }
18292
+ const projection2 = createProjection(
18293
+ projectionType,
18294
+ fullArea.width,
18295
+ mapAreaHeight,
18296
+ geoCollection,
18297
+ pointInset
18298
+ );
17991
18299
  const pathGen = path_default(projection2);
17992
18300
  if (projectionType !== "identity") {
17993
18301
  for (const feat of geoFeatures) {
@@ -18019,7 +18327,7 @@ function compileMap(spec, options) {
18019
18327
  compileWarnings.push(...filteredJoinWarnings);
18020
18328
  }
18021
18329
  const droppedFeatures = [];
18022
- const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
18330
+ const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber3;
18023
18331
  const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
18024
18332
  const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
18025
18333
  let featureMarks;
@@ -18047,7 +18355,7 @@ function compileMap(spec, options) {
18047
18355
  droppedFeatures
18048
18356
  });
18049
18357
  featureMarks = result.marks;
18050
- if (showLegend && continuousContent) {
18358
+ if (showChoroplethLegend && continuousContent) {
18051
18359
  const labelStyle = {
18052
18360
  fontFamily: theme.fonts.family,
18053
18361
  fontSize: theme.fonts.sizes.small,
@@ -18098,7 +18406,7 @@ function compileMap(spec, options) {
18098
18406
  mapAreaHeight,
18099
18407
  legendReserveGap,
18100
18408
  legendPosition,
18101
- showLegend,
18409
+ showLegend: showChoroplethLegend,
18102
18410
  droppedFeatures
18103
18411
  });
18104
18412
  featureMarks = result.marks;
@@ -18242,15 +18550,15 @@ function compileMap(spec, options) {
18242
18550
  }));
18243
18551
  const bottomChoroplethHeight = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
18244
18552
  const choroplethLegendGap = bottomChoroplethHeight > 0 ? 8 : 0;
18245
- const legendX = fullArea.x;
18246
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethHeight + choroplethLegendGap;
18247
- const legendWidth = fullArea.width;
18553
+ const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
18554
+ const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethHeight + choroplethLegendGap;
18555
+ const legendWidth = pointLegendOverlay ? fullArea.width - OVERLAY_LEGEND_INSET * 2 : fullArea.width;
18248
18556
  const swatchSize = 10;
18249
18557
  const swatchGap = 6;
18250
18558
  const entryGap = 16;
18251
18559
  pointCategoricalLegend = {
18252
18560
  type: "categorical",
18253
- position: "bottom",
18561
+ position: pointLegendOverlay ? "top-left" : "bottom",
18254
18562
  bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
18255
18563
  labelStyle,
18256
18564
  entries,
@@ -18290,8 +18598,8 @@ function compileMap(spec, options) {
18290
18598
  };
18291
18599
  const bottomChoroplethH = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
18292
18600
  const choroplethLegendGap = bottomChoroplethH > 0 ? 8 : 0;
18293
- const legendX = fullArea.x;
18294
- const legendY = fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
18601
+ const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
18602
+ const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
18295
18603
  const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
18296
18604
  const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
18297
18605
  const bounds = {
@@ -18309,7 +18617,7 @@ function compileMap(spec, options) {
18309
18617
  pointContinuousLegend = {
18310
18618
  type: "continuous",
18311
18619
  mode: ptContinuousContent.mode,
18312
- position: "bottom",
18620
+ position: pointLegendOverlay ? "top-left" : "bottom",
18313
18621
  bounds,
18314
18622
  labelStyle,
18315
18623
  bar,
@@ -18814,6 +19122,65 @@ import {
18814
19122
  resolveTheme as resolveTheme4
18815
19123
  } from "@opendata-ai/openchart-core";
18816
19124
 
19125
+ // src/graphs/animation.ts
19126
+ var ENTER_DEFAULT = {
19127
+ duration: 500,
19128
+ ease: "smooth",
19129
+ stagger: true,
19130
+ cameraFit: true
19131
+ };
19132
+ var UPDATE_DEFAULT = { duration: 300, ease: "smooth" };
19133
+ var EXIT_DEFAULT = { duration: 300, ease: "smooth" };
19134
+ var CAMERA_DEFAULT = { duration: "auto", ease: "smooth" };
19135
+ var HOVER_DEFAULT = { duration: 150, ease: "smooth" };
19136
+ function fullDefaults() {
19137
+ return {
19138
+ enter: { ...ENTER_DEFAULT },
19139
+ update: { ...UPDATE_DEFAULT },
19140
+ exit: { ...EXIT_DEFAULT },
19141
+ camera: { ...CAMERA_DEFAULT },
19142
+ hover: { ...HOVER_DEFAULT }
19143
+ };
19144
+ }
19145
+ function resolveGraphAnimation(spec) {
19146
+ if (spec === void 0 || spec === true) return fullDefaults();
19147
+ if (spec === false) return void 0;
19148
+ return {
19149
+ enter: resolveEnter(spec.enter),
19150
+ update: resolveDurationPhase(spec.update, UPDATE_DEFAULT),
19151
+ exit: resolveDurationPhase(spec.exit, EXIT_DEFAULT),
19152
+ camera: resolveCamera(spec.camera),
19153
+ hover: resolveDurationPhase(spec.hover, HOVER_DEFAULT)
19154
+ };
19155
+ }
19156
+ function resolveEnter(phase) {
19157
+ if (phase === false) return null;
19158
+ if (phase === void 0 || phase === true) return { ...ENTER_DEFAULT };
19159
+ return {
19160
+ duration: phase.duration ?? ENTER_DEFAULT.duration,
19161
+ ease: phase.ease ?? ENTER_DEFAULT.ease,
19162
+ stagger: phase.stagger !== false,
19163
+ cameraFit: ENTER_DEFAULT.cameraFit
19164
+ };
19165
+ }
19166
+ function resolveDurationPhase(phase, defaults) {
19167
+ if (phase === false) return null;
19168
+ if (phase === void 0 || phase === true)
19169
+ return { duration: defaults.duration, ease: defaults.ease };
19170
+ return {
19171
+ duration: phase.duration ?? defaults.duration,
19172
+ ease: phase.ease ?? defaults.ease
19173
+ };
19174
+ }
19175
+ function resolveCamera(phase) {
19176
+ if (phase === false) return null;
19177
+ if (phase === void 0 || phase === true) return { ...CAMERA_DEFAULT };
19178
+ return {
19179
+ duration: phase.duration ?? CAMERA_DEFAULT.duration,
19180
+ ease: phase.ease ?? CAMERA_DEFAULT.ease
19181
+ };
19182
+ }
19183
+
18817
19184
  // src/graphs/encoding.ts
18818
19185
  var DEFAULT_NODE_RADIUS = 5;
18819
19186
  var MIN_NODE_RADIUS = 3;
@@ -18853,16 +19220,42 @@ function computeDegrees(nodes, edges) {
18853
19220
  }
18854
19221
  return degrees2;
18855
19222
  }
19223
+ function resolveCategoricalDomain(values, sort, scaleDomain) {
19224
+ if (Array.isArray(scaleDomain) && scaleDomain.length > 0) {
19225
+ return scaleDomain.map((v) => String(v));
19226
+ }
19227
+ const seen = [];
19228
+ const set = /* @__PURE__ */ new Set();
19229
+ for (const v of values) {
19230
+ if (!set.has(v)) {
19231
+ set.add(v);
19232
+ seen.push(v);
19233
+ }
19234
+ }
19235
+ if (sort === null) return seen;
19236
+ if (Array.isArray(sort)) {
19237
+ const pinned = sort.filter((v) => set.has(v));
19238
+ const pinnedSet = new Set(pinned);
19239
+ const rest = seen.filter((v) => !pinnedSet.has(v));
19240
+ return [...pinned, ...rest];
19241
+ }
19242
+ const sorted = [...seen].sort((a, b) => a.localeCompare(b));
19243
+ return sort === "descending" ? sorted.reverse() : sorted;
19244
+ }
18856
19245
  function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18857
19246
  const degrees2 = computeDegrees(nodes, edges);
18858
19247
  const maxDegree = Math.max(1, ...degrees2.values());
18859
19248
  let sizeScale;
18860
19249
  if (encoding.nodeSize?.field) {
18861
19250
  const field = encoding.nodeSize.field;
19251
+ const scaleConfig = encoding.nodeSize.scale;
18862
19252
  const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
18863
19253
  const sizeMin = min(values) ?? 0;
18864
19254
  const sizeMax = max(values) ?? 1;
18865
- sizeScale = sqrt().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
19255
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [sizeMin, sizeMax];
19256
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [MIN_NODE_RADIUS, MAX_NODE_RADIUS];
19257
+ const ctor = scaleConfig?.type === "linear" ? linear2 : sqrt;
19258
+ sizeScale = ctor().domain(domain).range(range3);
18866
19259
  }
18867
19260
  let colorFn;
18868
19261
  if (encoding.nodeColor?.field) {
@@ -18883,13 +19276,39 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18883
19276
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
18884
19277
  };
18885
19278
  } else {
18886
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
19279
+ const domain = resolveCategoricalDomain(
19280
+ nodes.map((n) => String(n[field] ?? "")),
19281
+ encoding.nodeColor.sort,
19282
+ scaleConfig?.domain
19283
+ );
18887
19284
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18888
19285
  const ordinalScale = ordinal().domain(domain).range(range3);
18889
19286
  colorFn = (node) => ordinalScale(String(node[field] ?? ""));
18890
19287
  }
18891
19288
  }
18892
19289
  const defaultColor = theme.colors.categorical[0];
19290
+ let opacityScale;
19291
+ if (encoding.nodeOpacity?.field) {
19292
+ const field = encoding.nodeOpacity.field;
19293
+ const scaleConfig = encoding.nodeOpacity.scale;
19294
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19295
+ const opMin = min(values) ?? 0;
19296
+ const opMax = max(values) ?? 1;
19297
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [opMin, opMax];
19298
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0.25, 1];
19299
+ opacityScale = linear2().domain(domain).range(range3);
19300
+ }
19301
+ let labelPriorityScale;
19302
+ if (encoding.nodeLabelPriority?.field) {
19303
+ const field = encoding.nodeLabelPriority.field;
19304
+ const scaleConfig = encoding.nodeLabelPriority.scale;
19305
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19306
+ const lpMin = min(values) ?? 0;
19307
+ const lpMax = max(values) ?? 1;
19308
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [lpMin, lpMax];
19309
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0, 1];
19310
+ labelPriorityScale = linear2().domain(domain).range(range3);
19311
+ }
18893
19312
  return nodes.map((node) => {
18894
19313
  let radius = DEFAULT_NODE_RADIUS;
18895
19314
  if (sizeScale && encoding.nodeSize?.field) {
@@ -18898,6 +19317,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18898
19317
  radius = sizeScale(val);
18899
19318
  }
18900
19319
  }
19320
+ let opacity = 1;
19321
+ if (opacityScale && encoding.nodeOpacity?.field) {
19322
+ const val = Number(node[encoding.nodeOpacity.field]);
19323
+ if (Number.isFinite(val)) {
19324
+ opacity = opacityScale(val);
19325
+ }
19326
+ }
18901
19327
  const fill = colorFn ? colorFn(node) : defaultColor;
18902
19328
  const stroke = darkenColor(fill);
18903
19329
  let label;
@@ -18907,8 +19333,14 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18907
19333
  } else {
18908
19334
  label = node.id;
18909
19335
  }
18910
- const degree = degrees2.get(node.id) ?? 0;
18911
- const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19336
+ let labelPriority;
19337
+ if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
19338
+ const val = Number(node[encoding.nodeLabelPriority.field]);
19339
+ labelPriority = Number.isFinite(val) ? labelPriorityScale(val) : 0;
19340
+ } else {
19341
+ const degree = degrees2.get(node.id) ?? 0;
19342
+ labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19343
+ }
18912
19344
  const { id: _id, ...rest } = node;
18913
19345
  const data = { id: node.id, ...rest };
18914
19346
  const override = nodeOverrides?.[node.id];
@@ -18926,6 +19358,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18926
19358
  label,
18927
19359
  labelPriority: finalLabelPriority,
18928
19360
  community: void 0,
19361
+ opacity,
18929
19362
  data
18930
19363
  };
18931
19364
  });
@@ -18958,7 +19391,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
18958
19391
  return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
18959
19392
  };
18960
19393
  } else {
18961
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
19394
+ const domain = resolveCategoricalDomain(
19395
+ edges.map((e) => String(e[field] ?? "")),
19396
+ encoding.edgeColor.sort,
19397
+ scaleConfig?.domain
19398
+ );
18962
19399
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18963
19400
  const ordinalScale = ordinal().domain(domain).range(range3);
18964
19401
  edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
@@ -18969,10 +19406,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
18969
19406
  let styleFn;
18970
19407
  if (encoding.edgeStyle?.field) {
18971
19408
  const field = encoding.edgeStyle.field;
18972
- const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
19409
+ const domain = resolveCategoricalDomain(
19410
+ edges.map((e) => String(e[field] ?? "")),
19411
+ encoding.edgeStyle.sort,
19412
+ encoding.edgeStyle.scale?.domain
19413
+ );
18973
19414
  const styleMap = /* @__PURE__ */ new Map();
18974
- for (let i = 0; i < uniqueValues.length; i++) {
18975
- styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19415
+ for (let i = 0; i < domain.length; i++) {
19416
+ styleMap.set(domain[i], EDGE_STYLES[i % EDGE_STYLES.length]);
18976
19417
  }
18977
19418
  styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
18978
19419
  }
@@ -19030,12 +19471,49 @@ function applyCommunityColors(nodes, colorMap) {
19030
19471
  }
19031
19472
  }
19032
19473
 
19474
+ // src/graphs/interaction.ts
19475
+ var DEFAULT_DIM_OPACITY = 0.15;
19476
+ var DEFAULT_CURSOR_RADIUS = 80;
19477
+ var DEFAULT_CURSOR_STRENGTH = 30;
19478
+ function resolveGraphInteraction(cfg) {
19479
+ const hover = cfg?.hover;
19480
+ const cursor = cfg?.cursorRepulsion;
19481
+ let cursorRepulsion = null;
19482
+ if (cursor === true) {
19483
+ cursorRepulsion = { radius: DEFAULT_CURSOR_RADIUS, strength: DEFAULT_CURSOR_STRENGTH };
19484
+ } else if (cursor && typeof cursor === "object") {
19485
+ cursorRepulsion = {
19486
+ radius: cursor.radius ?? DEFAULT_CURSOR_RADIUS,
19487
+ strength: cursor.strength ?? DEFAULT_CURSOR_STRENGTH
19488
+ };
19489
+ }
19490
+ return {
19491
+ hoverMode: hover?.mode ?? "neighbors",
19492
+ dimOpacity: hover?.dimOpacity ?? DEFAULT_DIM_OPACITY,
19493
+ selectFlyTo: cfg?.select?.flyTo ?? false,
19494
+ cursorRepulsion,
19495
+ springyDrag: cfg?.springyDrag ?? false
19496
+ };
19497
+ }
19498
+
19033
19499
  // src/graphs/compile-graph.ts
19034
19500
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
19035
19501
  var SWATCH_SIZE2 = 12;
19036
19502
  var SWATCH_GAP2 = 6;
19037
19503
  var ENTRY_GAP2 = 16;
19038
- function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
19504
+ var ENERGY_PRESETS = {
19505
+ gentle: { chargeStrength: -150, velocityDecay: 0.5 },
19506
+ balanced: { chargeStrength: -300, velocityDecay: 0.4 },
19507
+ energetic: { chargeStrength: -600, velocityDecay: 0.3 }
19508
+ };
19509
+ var SETTLE_PRESETS = {
19510
+ quick: 0.05,
19511
+ balanced: 0.0228,
19512
+ thorough: 0.01
19513
+ };
19514
+ var DEFAULT_WARMUP_TICKS = 100;
19515
+ var DEFAULT_WARMUP_BUDGET_MS = 250;
19516
+ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField, legendOrder) {
19039
19517
  const labelStyle = {
19040
19518
  fontFamily: theme.fonts.family,
19041
19519
  fontSize: theme.fonts.sizes.small,
@@ -19045,25 +19523,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19045
19523
  };
19046
19524
  let entries;
19047
19525
  if (hasCommunities && communityColorMap.size > 0) {
19526
+ const counts = /* @__PURE__ */ new Map();
19527
+ for (const node of nodes) {
19528
+ if (node.community != null) {
19529
+ counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
19530
+ }
19531
+ }
19048
19532
  entries = [...communityColorMap.entries()].map(([label, color2]) => ({
19049
19533
  label,
19050
19534
  color: color2,
19051
19535
  shape: "circle",
19052
- active: true
19536
+ active: true,
19537
+ count: counts.get(label) ?? 0
19053
19538
  }));
19054
19539
  } else if (nodeColorField) {
19055
19540
  const categoryColors = /* @__PURE__ */ new Map();
19541
+ const categoryCounts = /* @__PURE__ */ new Map();
19056
19542
  for (const node of nodes) {
19057
19543
  const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
19058
19544
  if (!categoryColors.has(category)) {
19059
19545
  categoryColors.set(category, node.fill);
19060
19546
  }
19547
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19061
19548
  }
19062
- entries = categoryColors.size <= 1 ? [] : [...categoryColors.entries()].map(([label, color2]) => ({
19549
+ entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
19063
19550
  label,
19064
- color: color2,
19551
+ color: categoryColors.get(label),
19065
19552
  shape: "circle",
19066
- active: true
19553
+ active: true,
19554
+ count: categoryCounts.get(label) ?? 0
19067
19555
  }));
19068
19556
  } else {
19069
19557
  entries = [];
@@ -19079,6 +19567,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19079
19567
  swatchChipFill: theme.colors.annotationFill
19080
19568
  };
19081
19569
  }
19570
+ function buildEdgeLegend(edges, encoding) {
19571
+ const field = encoding.edgeColor?.field;
19572
+ if (!field || (encoding.edgeColor?.type ?? "nominal") === "quantitative") return void 0;
19573
+ const categoryColors = /* @__PURE__ */ new Map();
19574
+ const categoryCounts = /* @__PURE__ */ new Map();
19575
+ for (const edge of edges) {
19576
+ const category = String(edge.data[field] ?? "");
19577
+ if (!categoryColors.has(category)) categoryColors.set(category, edge.stroke);
19578
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19579
+ }
19580
+ if (categoryColors.size <= 1) return void 0;
19581
+ const order = resolveCategoricalDomain(
19582
+ edges.map((e) => String(e.data[field] ?? "")),
19583
+ encoding.edgeColor?.sort,
19584
+ encoding.edgeColor?.scale?.domain
19585
+ );
19586
+ return order.filter((label) => categoryColors.has(label)).map((label) => ({
19587
+ label,
19588
+ color: categoryColors.get(label),
19589
+ shape: "line",
19590
+ active: true,
19591
+ count: categoryCounts.get(label) ?? 0
19592
+ }));
19593
+ }
19082
19594
  function buildGraphTooltips(nodes) {
19083
19595
  const descriptors = /* @__PURE__ */ new Map();
19084
19596
  for (const node of nodes) {
@@ -19105,6 +19617,21 @@ function buildGraphTooltips(nodes) {
19105
19617
  }
19106
19618
  return descriptors;
19107
19619
  }
19620
+ function buildEdgeTooltip(edge) {
19621
+ const fields = [];
19622
+ for (const [key, value2] of Object.entries(edge.data)) {
19623
+ if (key === "source" || key === "target") continue;
19624
+ if (value2 == null) continue;
19625
+ fields.push({
19626
+ label: key,
19627
+ value: typeof value2 === "number" ? graphNumberFormatter(value2) : String(value2)
19628
+ });
19629
+ }
19630
+ return {
19631
+ title: `${edge.source} \u2192 ${edge.target}`,
19632
+ fields
19633
+ };
19634
+ }
19108
19635
  function compileGraph(spec, options) {
19109
19636
  const { spec: normalized, warnings } = compile(spec);
19110
19637
  emitSpecWarnings(warnings, options.onWarn);
@@ -19114,6 +19641,16 @@ function compileGraph(spec, options) {
19114
19641
  );
19115
19642
  }
19116
19643
  const graphSpec = normalized;
19644
+ for (const [name, ch] of [
19645
+ ["nodeColor", graphSpec.encoding.nodeColor],
19646
+ ["edgeColor", graphSpec.encoding.edgeColor]
19647
+ ]) {
19648
+ if (ch?.sort != null && ch.type === "quantitative") {
19649
+ options.onWarn?.(
19650
+ `encoding.${name}.sort is ignored for a quantitative field (sort only orders categorical domains).`
19651
+ );
19652
+ }
19653
+ }
19117
19654
  const rawWatermark = spec.watermark;
19118
19655
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
19119
19656
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
@@ -19139,13 +19676,30 @@ function compileGraph(spec, options) {
19139
19676
  }
19140
19677
  const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
19141
19678
  const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
19679
+ const nodeColorField = graphSpec.encoding.nodeColor?.field;
19680
+ const legendOrder = nodeColorField ? resolveCategoricalDomain(
19681
+ compiledNodes.map((n) => String(n.data[nodeColorField] ?? n.label ?? n.id)),
19682
+ graphSpec.encoding.nodeColor?.sort,
19683
+ graphSpec.encoding.nodeColor?.scale?.domain
19684
+ ) : void 0;
19142
19685
  const legend = buildGraphLegend(
19143
19686
  compiledNodes,
19144
19687
  communityColorMap,
19145
19688
  useCommunitiesForLegend,
19146
19689
  theme,
19147
- graphSpec.encoding.nodeColor?.field
19690
+ nodeColorField,
19691
+ legendOrder
19148
19692
  );
19693
+ const edgeLegend = buildEdgeLegend(compiledEdges, graphSpec.encoding);
19694
+ const legendField = nodeColorField ?? (useCommunitiesForLegend ? clusteringField ?? null : null);
19695
+ let initialHighlight;
19696
+ if (nodeColorField && graphSpec.encoding.nodeColor?.highlight != null) {
19697
+ const raw = graphSpec.encoding.nodeColor.highlight;
19698
+ const requested = Array.isArray(raw) ? raw : [raw];
19699
+ const available = new Set(legendOrder ?? []);
19700
+ const values = requested.filter((v) => available.has(v));
19701
+ if (values.length > 0) initialHighlight = { field: nodeColorField, values };
19702
+ }
19149
19703
  const tooltipDescriptors = buildGraphTooltips(compiledNodes);
19150
19704
  const communityCount = communityColorMap.size;
19151
19705
  const altParts = [
@@ -19162,17 +19716,26 @@ function compileGraph(spec, options) {
19162
19716
  };
19163
19717
  const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
19164
19718
  const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
19719
+ const energyPreset = graphSpec.layout.energy ? ENERGY_PRESETS[graphSpec.layout.energy] : ENERGY_PRESETS.balanced;
19720
+ const settlePreset = graphSpec.layout.settle ? SETTLE_PRESETS[graphSpec.layout.settle] : SETTLE_PRESETS.balanced;
19721
+ const warmupRaw = graphSpec.layout.warmup;
19722
+ const warmupTicks = warmupRaw === false ? 0 : typeof warmupRaw === "number" ? Math.max(0, Math.floor(warmupRaw)) : DEFAULT_WARMUP_TICKS;
19165
19723
  const simulationConfig = {
19166
- chargeStrength: graphSpec.layout.chargeStrength ?? -300,
19724
+ chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
19167
19725
  linkDistance: graphSpec.layout.linkDistance ?? 30,
19168
19726
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
19169
- alphaDecay: 0.0228,
19170
- velocityDecay: 0.4,
19727
+ alphaDecay: settlePreset,
19728
+ velocityDecay: energyPreset.velocityDecay,
19171
19729
  collisionRadius: maxRadius + collisionPadding,
19172
19730
  collisionPadding,
19173
19731
  linkStrength: graphSpec.layout.linkStrength,
19174
- centerForce: graphSpec.layout.centerForce
19732
+ centerForce: graphSpec.layout.centerForce,
19733
+ seed: graphSpec.layout.seed,
19734
+ warmupTicks,
19735
+ warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
19175
19736
  };
19737
+ const animation = resolveGraphAnimation(graphSpec.animation);
19738
+ const interaction = resolveGraphInteraction(graphSpec.interaction);
19176
19739
  const chrome = computeChrome4(
19177
19740
  {
19178
19741
  title: graphSpec.chrome.title,
@@ -19201,7 +19764,12 @@ function compileGraph(spec, options) {
19201
19764
  height: options.height
19202
19765
  },
19203
19766
  simulationConfig,
19204
- watermark
19767
+ watermark,
19768
+ animation,
19769
+ interaction,
19770
+ legendField,
19771
+ initialHighlight,
19772
+ edgeLegend
19205
19773
  };
19206
19774
  }
19207
19775
  var DEFAULT_COLLISION_PADDING = 5;
@@ -19294,6 +19862,7 @@ function legendLayoutHasContent(legendLayout) {
19294
19862
  }
19295
19863
  var MIN_CHART_WIDTH = 60;
19296
19864
  var MIN_CHART_HEIGHT2 = 40;
19865
+ var PLOT_SHARE_MAX = 0.4;
19297
19866
  var SERIES_SEARCH_INPUT_HEIGHT = 32;
19298
19867
  var SERIES_SEARCH_GAP = 12;
19299
19868
  function getMinChartDims(display) {
@@ -19336,8 +19905,10 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19336
19905
  watermark,
19337
19906
  bottomLegendReservation
19338
19907
  );
19908
+ const chromeLayout = resolveChromeLayout(spec, options);
19909
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
19339
19910
  if (isSparkline) {
19340
- const total2 = { x: 0, y: 0, width, height };
19911
+ const total2 = { x: 0, y: 0, width, height: effectiveHeight };
19341
19912
  const sparkPad = getSparklinePad(spec);
19342
19913
  const xAxisSpace = userExplicit.xAxis ? 26 : 0;
19343
19914
  const yAxisSpace = userExplicit.yAxis ? 30 : 0;
@@ -19361,7 +19932,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19361
19932
  x: margins2.left,
19362
19933
  y: margins2.top,
19363
19934
  width: Math.max(0, width - margins2.left - margins2.right),
19364
- height: Math.max(0, height - margins2.top - margins2.bottom)
19935
+ height: Math.max(0, effectiveHeight - margins2.top - margins2.bottom)
19365
19936
  };
19366
19937
  return {
19367
19938
  total: total2,
@@ -19373,7 +19944,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19373
19944
  effectiveAxisGap: 0
19374
19945
  };
19375
19946
  }
19376
- const total = { x: 0, y: 0, width, height };
19947
+ const total = { x: 0, y: 0, width, height: effectiveHeight };
19377
19948
  const isRadial = isAxislessMark2(spec.markType);
19378
19949
  const encoding = spec.encoding;
19379
19950
  const xAxisSuppressed = encoding.x?.axis === false;
@@ -19603,10 +20174,11 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19603
20174
  x: margins.left,
19604
20175
  y: margins.top,
19605
20176
  width: Math.max(0, width - margins.left - margins.right),
19606
- height: Math.max(0, height - margins.top - margins.bottom)
20177
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19607
20178
  };
19608
20179
  const minDims = getMinChartDims(spec.display);
19609
- if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
20180
+ const chromeEatsPlot = chromeLayout !== "grow" && chrome.topHeight > height * PLOT_SHARE_MAX;
20181
+ if ((chartArea.width < minDims.width || chartArea.height < minDims.height || chromeEatsPlot) && chromeMode !== "hidden") {
19610
20182
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
19611
20183
  const fallbackChrome = computeChrome5(
19612
20184
  chromeToInput(spec.chrome),
@@ -19632,7 +20204,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19632
20204
  x: margins.left,
19633
20205
  y: margins.top,
19634
20206
  width: Math.max(0, width - margins.left - margins.right),
19635
- height: Math.max(0, height - margins.top - margins.bottom)
20207
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19636
20208
  };
19637
20209
  const fallbackMetricsTopY = topPad + fallbackChrome.topHeight;
19638
20210
  const fallbackMetricsArea = { x: hPad, width: Math.max(0, width - hPad * 2) };
@@ -19649,7 +20221,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19649
20221
  chartArea = {
19650
20222
  ...chartArea,
19651
20223
  y: margins.top,
19652
- height: Math.max(0, height - margins.top - margins.bottom)
20224
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19653
20225
  };
19654
20226
  }
19655
20227
  return {
@@ -19665,7 +20237,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19665
20237
  fallbackMetricsArea
19666
20238
  ) : void 0,
19667
20239
  xAxisHeight,
19668
- effectiveAxisGap: fallbackEffectiveAxisGap
20240
+ effectiveAxisGap: fallbackEffectiveAxisGap,
20241
+ chromeStripped: true
19669
20242
  };
19670
20243
  }
19671
20244
  }
@@ -19677,7 +20250,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19677
20250
  chartArea = {
19678
20251
  ...chartArea,
19679
20252
  y: margins.top,
19680
- height: Math.max(0, height - margins.top - margins.bottom)
20253
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19681
20254
  };
19682
20255
  }
19683
20256
  return {
@@ -20429,14 +21002,19 @@ function compileSankey(spec, options) {
20429
21002
  watermark
20430
21003
  );
20431
21004
  const padding = theme.spacing.padding;
21005
+ const chromeLayout = resolveChromeLayout(
21006
+ spec,
21007
+ options
21008
+ );
21009
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
20432
21010
  const fullArea = {
20433
21011
  x: padding,
20434
21012
  y: padding + chrome.topHeight,
20435
21013
  width: options.width - padding * 2,
20436
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
21014
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
20437
21015
  };
20438
21016
  if (fullArea.width <= 0 || fullArea.height <= 0) {
20439
- return emptyLayout4(fullArea, chrome, theme, options, watermark);
21017
+ return emptyLayout4(fullArea, chrome, theme, { ...options, height: grownHeight }, watermark);
20440
21018
  }
20441
21019
  const sourceField = sankeySpec.encoding.source.field;
20442
21020
  const targetField = sankeySpec.encoding.target.field;
@@ -20476,7 +21054,7 @@ function compileSankey(spec, options) {
20476
21054
  height: fullArea.height - legend.bounds.height - legendGap2
20477
21055
  };
20478
21056
  if (area.height <= 0) {
20479
- return emptyLayout4(area, chrome, theme, options, watermark);
21057
+ return emptyLayout4(area, chrome, theme, { ...options, height: grownHeight }, watermark);
20480
21058
  }
20481
21059
  const labelFontSize = theme.fonts.sizes.small;
20482
21060
  const labelFontWeight = theme.fonts.weights.normal;
@@ -20628,7 +21206,7 @@ function compileSankey(spec, options) {
20628
21206
  theme,
20629
21207
  dimensions: {
20630
21208
  width: options.width,
20631
- height: options.height
21209
+ height: grownHeight
20632
21210
  },
20633
21211
  animation: resolvedAnimation,
20634
21212
  watermark,
@@ -21447,6 +22025,10 @@ var LEGEND_ENTRY_GAP = 16;
21447
22025
  function clamp(value2, min4, max4) {
21448
22026
  return Math.min(max4, Math.max(min4, value2));
21449
22027
  }
22028
+ function growContentHeight(contentHeight, chromeLayout, budgetHeight, chrome) {
22029
+ if (chromeLayout !== "grow") return contentHeight;
22030
+ return Math.max(contentHeight, budgetHeight + chrome.topHeight + chrome.bottomHeight);
22031
+ }
21450
22032
  function compileTileMap(spec, options) {
21451
22033
  const sugarWarnings = [];
21452
22034
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
@@ -21483,22 +22065,44 @@ function compileTileMap(spec, options) {
21483
22065
  watermark
21484
22066
  );
21485
22067
  const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
22068
+ const chromeLayout = resolveChromeLayout(
22069
+ spec,
22070
+ options
22071
+ );
21486
22072
  const fullArea = {
21487
22073
  x: padding,
21488
22074
  y: padding + chrome.topHeight,
21489
22075
  width: options.width - padding * 2,
21490
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
22076
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
21491
22077
  };
21492
22078
  if (fullArea.width <= 0 || fullArea.height <= 0) {
21493
22079
  return emptyLayout5(chrome, theme, options, watermark);
21494
22080
  }
21495
22081
  const isCategorical = tilemapSpec.encoding.color !== void 0 || tilemapSpec.colors !== void 0;
21496
22082
  if (isCategorical) {
21497
- return compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
22083
+ return compileCategorical(
22084
+ tilemapSpec,
22085
+ options,
22086
+ theme,
22087
+ chrome,
22088
+ fullArea,
22089
+ isDarkMode,
22090
+ watermark,
22091
+ chromeLayout
22092
+ );
21498
22093
  }
21499
- return compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
22094
+ return compileQuantitative(
22095
+ tilemapSpec,
22096
+ options,
22097
+ theme,
22098
+ chrome,
22099
+ fullArea,
22100
+ isDarkMode,
22101
+ watermark,
22102
+ chromeLayout
22103
+ );
21500
22104
  }
21501
- function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22105
+ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21502
22106
  const stateField = tilemapSpec.encoding.state.field;
21503
22107
  const valueField = tilemapSpec.encoding.value.field;
21504
22108
  const stateValueMap = /* @__PURE__ */ new Map();
@@ -21599,7 +22203,12 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21599
22203
  };
21600
22204
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21601
22205
  const padding = fullArea.x;
21602
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22206
+ const contentHeight = growContentHeight(
22207
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22208
+ chromeLayout,
22209
+ options.height,
22210
+ chrome
22211
+ );
21603
22212
  return {
21604
22213
  area: fullArea,
21605
22214
  chrome,
@@ -21616,7 +22225,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21616
22225
  measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth23(text, fontSize), height: fontSize }))
21617
22226
  };
21618
22227
  }
21619
- function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22228
+ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21620
22229
  const stateField = tilemapSpec.encoding.state.field;
21621
22230
  const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
21622
22231
  const stateCategoryMap = /* @__PURE__ */ new Map();
@@ -21735,7 +22344,12 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
21735
22344
  };
21736
22345
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21737
22346
  const padding = fullArea.x;
21738
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22347
+ const contentHeight = growContentHeight(
22348
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22349
+ chromeLayout,
22350
+ options.height,
22351
+ chrome
22352
+ );
21739
22353
  return {
21740
22354
  area: fullArea,
21741
22355
  chrome,
@@ -22795,6 +23409,38 @@ function applySparklineDefaults(spec, theme) {
22795
23409
  }
22796
23410
  return { ...spec, markDef: newMarkDef };
22797
23411
  }
23412
+ function resolveScaleInvert(resolved) {
23413
+ if (!resolved) return void 0;
23414
+ const { scale, type } = resolved;
23415
+ if (type === "band" || type === "point") {
23416
+ const domain = scale.domain();
23417
+ const positions = domain.map((v) => {
23418
+ const pos = scale(v) ?? 0;
23419
+ return type === "band" ? pos + scale.bandwidth() / 2 : pos;
23420
+ });
23421
+ const range4 = scale.range();
23422
+ return {
23423
+ topPixel: range4[0],
23424
+ bottomPixel: range4[range4.length - 1],
23425
+ topData: 0,
23426
+ bottomData: 0,
23427
+ domain,
23428
+ positions
23429
+ };
23430
+ }
23431
+ const invert = scale.invert;
23432
+ if (typeof invert !== "function") return void 0;
23433
+ const range3 = scale.range();
23434
+ const topPx = range3[0];
23435
+ const bottomPx = range3[range3.length - 1];
23436
+ const topVal = invert.call(scale, topPx);
23437
+ const bottomVal = invert.call(scale, bottomPx);
23438
+ const topData = topVal instanceof Date ? +topVal : Number(topVal);
23439
+ const bottomData = bottomVal instanceof Date ? +bottomVal : Number(bottomVal);
23440
+ if (!Number.isFinite(topData) || !Number.isFinite(bottomData)) return void 0;
23441
+ const scaleType = type;
23442
+ return { topPixel: topPx, bottomPixel: bottomPx, topData, bottomData, scaleType };
23443
+ }
22798
23444
  function compileChart(spec, optionsInput) {
22799
23445
  const sugarWarnings = [];
22800
23446
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
@@ -22811,7 +23457,7 @@ function compileChart(spec, optionsInput) {
22811
23457
  throw new Error("compileChart received a sankey spec. Use compileSankey instead.");
22812
23458
  }
22813
23459
  if ("type" in normalized && normalized.type === "map") {
22814
- throw new Error("compileChart received a map spec. Use compileMap instead.");
23460
+ throw new Error("compileChart received a map spec. Use compileGeoMap instead.");
22815
23461
  }
22816
23462
  let chartSpec = normalized;
22817
23463
  const rawWatermark = expandedSpec.watermark;
@@ -22995,12 +23641,35 @@ function compileChart(spec, optionsInput) {
22995
23641
  }
22996
23642
  };
22997
23643
  }
23644
+ let facetDirection = "wrap";
23645
+ const rowChannel = renderSpec.encoding.row;
23646
+ const colChannel = renderSpec.encoding.column;
23647
+ if (rowChannel?.field || colChannel?.field) {
23648
+ const channel = rowChannel?.field ? rowChannel : colChannel;
23649
+ facetDirection = rowChannel?.field ? "row" : "column";
23650
+ const desugared = { ...renderSpec.encoding };
23651
+ delete desugared.row;
23652
+ delete desugared.column;
23653
+ desugared.facet = {
23654
+ field: channel.field,
23655
+ type: channel.type,
23656
+ sort: channel.sort,
23657
+ ...facetDirection === "row" ? { columns: 1 } : {}
23658
+ };
23659
+ renderSpec = { ...renderSpec, encoding: desugared };
23660
+ const chartDesugared = { ...chartSpec.encoding };
23661
+ delete chartDesugared.row;
23662
+ delete chartDesugared.column;
23663
+ chartDesugared.facet = desugared.facet;
23664
+ chartSpec = { ...chartSpec, encoding: chartDesugared };
23665
+ }
22998
23666
  const facetChannel = renderSpec.encoding.facet;
22999
23667
  if (facetChannel?.field) {
23000
23668
  return compileFaceted(
23001
23669
  chartSpec,
23002
23670
  renderSpec,
23003
23671
  facetChannel,
23672
+ facetDirection,
23004
23673
  options,
23005
23674
  theme,
23006
23675
  strategy,
@@ -23065,7 +23734,10 @@ function compileChart(spec, optionsInput) {
23065
23734
  skipY,
23066
23735
  markType: chartSpec.markType,
23067
23736
  totalWidth: options.width,
23068
- precomputedYTicks: plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23737
+ // If the guardrail stripped chrome, the plan's y-ticks were sized
23738
+ // against the pre-strip (shorter) plot. Drop them so computeAxes
23739
+ // regenerates against the final (taller) chartArea.
23740
+ precomputedYTicks: !dims.chromeStripped && plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23069
23741
  });
23070
23742
  const xAxisExtent = axes.x?.extent ?? 0;
23071
23743
  dims.chrome.bottomAnchorY = chartArea.y + chartArea.height + xAxisExtent;
@@ -23080,6 +23752,19 @@ function compileChart(spec, optionsInput) {
23080
23752
  const renderer = getChartRenderer(rendererKey);
23081
23753
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
23082
23754
  applyFillPatterns(marks, chartSpec.markDef, theme);
23755
+ const markRenderModeWarnings = [];
23756
+ const markRenderMode = resolveMarkRenderMode(
23757
+ {
23758
+ requested: options.renderer,
23759
+ markType: chartSpec.markType,
23760
+ pointCount: marks.reduce((n, m) => m.type === "point" ? n + 1 : n, 0),
23761
+ display: chartSpec.display,
23762
+ faceted: false,
23763
+ layered: options.layered ?? false
23764
+ },
23765
+ markRenderModeWarnings
23766
+ );
23767
+ emitSpecWarnings(markRenderModeWarnings, options.onWarn);
23083
23768
  const xFieldForYdi = chartSpec.encoding.x && "field" in chartSpec.encoding.x ? chartSpec.encoding.x.field : void 0;
23084
23769
  const youDrawIt = resolveYouDrawIt(chartSpec.youDrawIt, marks, scales, chartArea, xFieldForYdi);
23085
23770
  const endpointLabels = computeEndpointLabels(
@@ -23160,6 +23845,8 @@ function compileChart(spec, optionsInput) {
23160
23845
  );
23161
23846
  assignAnimationIndices(marks, resolvedAnimation);
23162
23847
  const chrome = footnotes ? { ...dims.chrome, footnotes } : dims.chrome;
23848
+ const xInvert = resolveScaleInvert(scales.x);
23849
+ const yInvert = resolveScaleInvert(scales.y);
23163
23850
  return {
23164
23851
  area: chartArea,
23165
23852
  chrome,
@@ -23179,6 +23866,7 @@ function compileChart(spec, optionsInput) {
23179
23866
  a11y: {
23180
23867
  altText,
23181
23868
  dataTableFallback,
23869
+ ...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
23182
23870
  role: "img",
23183
23871
  keyboardNavigable: marks.length > 0,
23184
23872
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
@@ -23186,16 +23874,22 @@ function compileChart(spec, optionsInput) {
23186
23874
  theme,
23187
23875
  dimensions: {
23188
23876
  width: options.width,
23189
- height: options.height
23877
+ // In chromeLayout 'grow' mode, computeDimensions grows total.height by the
23878
+ // chrome height so the plot keeps its full budget. In the default
23879
+ // 'subtract' mode total.height === options.height, so this is a no-op.
23880
+ height: dims.total.height
23190
23881
  },
23191
23882
  animation: resolvedAnimation,
23192
23883
  watermark,
23193
23884
  display: chartSpec.display,
23194
23885
  crosshair,
23195
- measureText: options.measureText
23886
+ measureText: options.measureText,
23887
+ ...xInvert ? { xInvert } : {},
23888
+ ...yInvert ? { yInvert } : {},
23889
+ ...markRenderMode === "canvas" ? { markRenderMode } : {}
23196
23890
  };
23197
23891
  }
23198
- function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
23892
+ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
23199
23893
  const measure = createMeasureFn(options.measureText);
23200
23894
  const panelEncoding = { ...renderSpec.encoding };
23201
23895
  delete panelEncoding.facet;
@@ -23241,10 +23935,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23241
23935
  plan.xAxisExtent,
23242
23936
  dims.effectiveAxisGap
23243
23937
  );
23244
- const leftReservation = plan.leftGutter;
23245
23938
  const bottomReservation = plan.xAxisExtent;
23246
- const grid = computeFacetGrid(facetValues, facetChannel.columns, chartArea, {
23247
- left: leftReservation,
23939
+ const effectiveColumns = facetDirection === "column" ? facetValues.length : facetChannel.columns;
23940
+ const grid = computeFacetGrid(facetValues, effectiveColumns, chartArea, {
23941
+ left: 0,
23248
23942
  bottom: bottomReservation
23249
23943
  });
23250
23944
  if (!optionsInput.facetHeightGrown && grid.panels.length > 0 && grid.panels[0].area.height < MIN_PANEL_HEIGHT) {
@@ -23259,8 +23953,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23259
23953
  }
23260
23954
  }
23261
23955
  const resolveConfig = chartSpec.resolve;
23262
- const yResolve = resolveConfig?.scale?.y ?? "shared";
23263
- const xResolve = resolveConfig?.scale?.x ?? "shared";
23956
+ const xDefault = facetDirection === "column" ? "independent" : "shared";
23957
+ const yDefault = facetDirection === "row" ? "independent" : "shared";
23958
+ const yResolve = resolveConfig?.scale?.y ?? yDefault;
23959
+ const xResolve = resolveConfig?.scale?.x ?? xDefault;
23264
23960
  const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
23265
23961
  const sharedXDomain = xResolve === "shared" && fullScales.x ? fullScales.x.scale.domain() : void 0;
23266
23962
  const sharedYDomain = yResolve === "shared" && fullScales.y ? fullScales.y.scale.domain() : void 0;
@@ -23325,6 +24021,16 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23325
24021
  tickMarks: false
23326
24022
  };
23327
24023
  }
24024
+ const isBottomRow = gridPanel.row === grid.rows - 1;
24025
+ if (xResolve === "shared" && facetDirection === "row" && !isBottomRow && panelAxes.x) {
24026
+ panelAxes.x = {
24027
+ ...panelAxes.x,
24028
+ ticks: [],
24029
+ label: void 0,
24030
+ domainLine: false,
24031
+ tickMarks: false
24032
+ };
24033
+ }
23328
24034
  const panelMarks = renderer ? renderer(
23329
24035
  panelSpecWithDomains,
23330
24036
  panelScales,
@@ -23378,10 +24084,11 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23378
24084
  annotations: panelAnnotations,
23379
24085
  header: {
23380
24086
  text: gridPanel.key,
23381
- x: gridPanel.headerPos.x,
24087
+ x: facetDirection === "row" ? gridPanel.area.x : gridPanel.headerPos.x,
23382
24088
  y: gridPanel.headerPos.y,
23383
24089
  fontSize: theme.fonts.sizes.body,
23384
- fontWeight: theme.fonts.weights.medium
24090
+ fontWeight: theme.fonts.weights.medium,
24091
+ ...facetDirection === "row" ? { textAnchor: "start" } : {}
23385
24092
  }
23386
24093
  });
23387
24094
  }
@@ -23418,6 +24125,19 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23418
24125
  chartSpec.data
23419
24126
  );
23420
24127
  assignAnimationIndices(allMarks, resolvedAnimation);
24128
+ const facetRenderModeWarnings = [];
24129
+ resolveMarkRenderMode(
24130
+ {
24131
+ requested: options.renderer,
24132
+ markType: chartSpec.markType,
24133
+ pointCount: allMarks.reduce((n, m) => m.type === "point" ? n + 1 : n, 0),
24134
+ display: chartSpec.display,
24135
+ faceted: true,
24136
+ layered: options.layered ?? false
24137
+ },
24138
+ facetRenderModeWarnings
24139
+ );
24140
+ emitSpecWarnings(facetRenderModeWarnings, options.onWarn);
23421
24141
  return {
23422
24142
  area: chartArea,
23423
24143
  chrome: allFootnotes.length > 0 ? { ...dims.chrome, footnotes: allFootnotes } : dims.chrome,
@@ -23434,6 +24154,7 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23434
24154
  a11y: {
23435
24155
  altText,
23436
24156
  dataTableFallback,
24157
+ ...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
23437
24158
  role: "img",
23438
24159
  keyboardNavigable: allMarks.length > 0,
23439
24160
  ...chartSpec.a11y?.hidden ? { hidden: true } : {}
@@ -23449,7 +24170,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
23449
24170
  panels: panelLayouts,
23450
24171
  facetField: facetChannel.field,
23451
24172
  columns: grid.columns,
23452
- sharedScales: yResolve === "shared" && xResolve === "shared"
24173
+ sharedScales: yResolve === "shared" && xResolve === "shared",
24174
+ direction: facetDirection
23453
24175
  }
23454
24176
  };
23455
24177
  }
@@ -23488,8 +24210,8 @@ function compileSankey2(spec, options) {
23488
24210
  function compileTileMap2(spec, options) {
23489
24211
  return compileTileMap(spec, options);
23490
24212
  }
23491
- function compileMap2(spec, options) {
23492
- return compileMap(spec, options);
24213
+ function compileGeoMap2(spec, options) {
24214
+ return compileGeoMap(spec, options);
23493
24215
  }
23494
24216
  function compileBarList2(spec, options) {
23495
24217
  return compileBarList(spec, options);
@@ -23503,15 +24225,16 @@ export {
23503
24225
  EXIT_DEFAULTS,
23504
24226
  UPDATE_DEFAULTS,
23505
24227
  buildAreaPath,
24228
+ buildEdgeTooltip,
23506
24229
  buildLinePath,
23507
24230
  clampStaggerDelay,
23508
24231
  clearRenderers,
23509
24232
  compile,
23510
24233
  compileBarList2 as compileBarList,
23511
24234
  compileChart,
24235
+ compileGeoMap2 as compileGeoMap,
23512
24236
  compileGraph2 as compileGraph,
23513
24237
  compileLayer2 as compileLayer,
23514
- compileMap2 as compileMap,
23515
24238
  compileSankey2 as compileSankey,
23516
24239
  compileTable,
23517
24240
  compileTileMap2 as compileTileMap,