@opendata-ai/openchart-engine 8.0.0-rc.11 → 8.0.0-rc.13

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.
package/dist/index.js CHANGED
@@ -10482,6 +10482,7 @@ function normalizeChartSpec(spec, warnings) {
10482
10482
  data: spec.data,
10483
10483
  encoding,
10484
10484
  chrome: normalizeChrome(spec.chrome),
10485
+ chromeLayout: spec.chromeLayout,
10485
10486
  metrics: spec.metrics,
10486
10487
  seriesSearch: normalizeSeriesSearch(spec, encoding, warnings),
10487
10488
  youDrawIt: normalizeYouDrawIt(spec),
@@ -10556,15 +10557,10 @@ function normalizeSankeySpec(spec, _warnings) {
10556
10557
  };
10557
10558
  }
10558
10559
  function normalizeGraphSpec(spec) {
10559
- const defaultLayout = {
10560
+ const layout = {
10560
10561
  type: "force",
10561
- chargeStrength: -300,
10562
- linkDistance: 30
10563
- };
10564
- const layout = spec.layout ? {
10565
- ...defaultLayout,
10566
10562
  ...spec.layout
10567
- } : defaultLayout;
10563
+ };
10568
10564
  return {
10569
10565
  type: "graph",
10570
10566
  nodes: spec.nodes,
@@ -10576,7 +10572,10 @@ function normalizeGraphSpec(spec) {
10576
10572
  annotations: normalizeAnnotations(spec.annotations),
10577
10573
  theme: spec.theme ?? {},
10578
10574
  darkMode: spec.darkMode ?? "off",
10579
- watermark: spec.watermark ?? true
10575
+ watermark: spec.watermark ?? true,
10576
+ animation: spec.animation,
10577
+ interaction: spec.interaction,
10578
+ legend: spec.legend
10580
10579
  };
10581
10580
  }
10582
10581
  function normalizeTileMapSpec(spec, warnings) {
@@ -11605,14 +11604,22 @@ function validateGraphSpec(spec, errors) {
11605
11604
  }
11606
11605
  if (spec.encoding && typeof spec.encoding === "object") {
11607
11606
  const encoding = spec.encoding;
11608
- const firstNode = nodes[0];
11609
- const firstEdge = edges.length > 0 ? edges[0] : null;
11610
- const nodeFields = firstNode ? new Set(Object.keys(firstNode)) : /* @__PURE__ */ new Set();
11611
- const edgeFields = firstEdge ? new Set(Object.keys(firstEdge)) : /* @__PURE__ */ new Set();
11612
- const nodeChannels = ["nodeColor", "nodeSize", "nodeLabel"];
11607
+ const nodeFields = /* @__PURE__ */ new Set();
11608
+ for (const node of nodes) {
11609
+ if (node && typeof node === "object") {
11610
+ for (const key of Object.keys(node)) nodeFields.add(key);
11611
+ }
11612
+ }
11613
+ const edgeFields = /* @__PURE__ */ new Set();
11614
+ for (const edge of edges) {
11615
+ if (edge && typeof edge === "object") {
11616
+ for (const key of Object.keys(edge)) edgeFields.add(key);
11617
+ }
11618
+ }
11619
+ const nodeChannels = ["nodeColor", "nodeSize", "nodeOpacity", "nodeLabel"];
11613
11620
  for (const channel of nodeChannels) {
11614
11621
  const ch = encoding[channel];
11615
- if (ch?.field && typeof ch.field === "string" && !nodeFields.has(ch.field)) {
11622
+ if (ch?.field && typeof ch.field === "string" && nodes.length > 0 && !nodeFields.has(ch.field)) {
11616
11623
  errors.push({
11617
11624
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
11618
11625
  path: `encoding.${channel}.field`,
@@ -11621,10 +11628,10 @@ function validateGraphSpec(spec, errors) {
11621
11628
  });
11622
11629
  }
11623
11630
  }
11624
- const edgeChannels = ["edgeColor", "edgeWidth"];
11631
+ const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
11625
11632
  for (const channel of edgeChannels) {
11626
11633
  const ch = encoding[channel];
11627
- if (ch?.field && typeof ch.field === "string" && firstEdge && !edgeFields.has(ch.field)) {
11634
+ if (ch?.field && typeof ch.field === "string" && edges.length > 0 && !edgeFields.has(ch.field)) {
11628
11635
  errors.push({
11629
11636
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
11630
11637
  path: `encoding.${channel}.field`,
@@ -12348,6 +12355,36 @@ ${errorMessages}`);
12348
12355
  return { spec: normalized, warnings };
12349
12356
  }
12350
12357
 
12358
+ // src/layout/shared.ts
12359
+ function chromeToInput(chrome) {
12360
+ return {
12361
+ eyebrow: chrome.eyebrow,
12362
+ title: chrome.title,
12363
+ subtitle: chrome.subtitle,
12364
+ source: chrome.source,
12365
+ byline: chrome.byline,
12366
+ footer: chrome.footer,
12367
+ brand: chrome.brand
12368
+ };
12369
+ }
12370
+ function scalePadding(basePadding, width, height) {
12371
+ const minDim = Math.min(width, height);
12372
+ if (minDim >= 500) return basePadding;
12373
+ if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
12374
+ const t = (minDim - 200) / 300;
12375
+ return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
12376
+ }
12377
+ function bottomMargin(bottomHeight, padding, xAxisHeight) {
12378
+ return padding + bottomHeight + xAxisHeight;
12379
+ }
12380
+ var INLINE_TICK_OVERHANG_PAD = 6;
12381
+ function resolveChromeLayout(spec, options) {
12382
+ const encoding = spec?.encoding;
12383
+ const isFaceted = !!(encoding?.facet || encoding?.row || encoding?.column);
12384
+ if (isFaceted) return "subtract";
12385
+ return spec?.chromeLayout ?? options?.chromeLayout ?? "subtract";
12386
+ }
12387
+
12351
12388
  // src/barlist/compile-barlist.ts
12352
12389
  var DEFAULT_ROW_GAP = 8;
12353
12390
  var LABEL_BAR_GAP = 12;
@@ -12395,14 +12432,19 @@ function compileBarList(spec, options) {
12395
12432
  watermark
12396
12433
  );
12397
12434
  const padding = theme.spacing.padding;
12435
+ const chromeLayout = resolveChromeLayout(
12436
+ spec,
12437
+ options
12438
+ );
12439
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
12398
12440
  const fullArea = {
12399
12441
  x: padding,
12400
12442
  y: padding + chrome.topHeight,
12401
12443
  width: options.width - padding * 2,
12402
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12444
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
12403
12445
  };
12404
12446
  if (fullArea.width <= 0 || fullArea.height <= 0) {
12405
- return emptyLayout2(chrome, theme, options, watermark);
12447
+ return emptyLayout2(chrome, theme, { ...options, height: grownHeight }, watermark);
12406
12448
  }
12407
12449
  const labelField = barlistSpec.encoding.label.field;
12408
12450
  const valueField = barlistSpec.encoding.value.field;
@@ -12587,7 +12629,7 @@ function compileBarList(spec, options) {
12587
12629
  a11y,
12588
12630
  theme,
12589
12631
  width: options.width,
12590
- height: options.height,
12632
+ height: grownHeight,
12591
12633
  animation: resolvedAnimation,
12592
12634
  watermark,
12593
12635
  measureText
@@ -14881,30 +14923,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
14881
14923
  return result;
14882
14924
  }
14883
14925
 
14884
- // src/layout/shared.ts
14885
- function chromeToInput(chrome) {
14886
- return {
14887
- eyebrow: chrome.eyebrow,
14888
- title: chrome.title,
14889
- subtitle: chrome.subtitle,
14890
- source: chrome.source,
14891
- byline: chrome.byline,
14892
- footer: chrome.footer,
14893
- brand: chrome.brand
14894
- };
14895
- }
14896
- function scalePadding(basePadding, width, height) {
14897
- const minDim = Math.min(width, height);
14898
- if (minDim >= 500) return basePadding;
14899
- if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
14900
- const t = (minDim - 200) / 300;
14901
- return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
14902
- }
14903
- function bottomMargin(bottomHeight, padding, xAxisHeight) {
14904
- return padding + bottomHeight + xAxisHeight;
14905
- }
14906
- var INLINE_TICK_OVERHANG_PAD = 6;
14907
-
14908
14926
  // src/layout/plan.ts
14909
14927
  function createMeasureFn(measureText) {
14910
14928
  if (measureText) {
@@ -15126,7 +15144,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
15126
15144
  if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
15127
15145
  topMargin += effectiveTopAxisGap;
15128
15146
  const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
15129
- const chartHeight = Math.max(0, height - topMargin - bMargin);
15147
+ const chromeLayout = resolveChromeLayout(renderSpec, options);
15148
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
15149
+ const chartHeight = Math.max(0, effectiveHeight - topMargin - bMargin);
15130
15150
  const rightMargin = hPad + (isRadial ? hPad : axisMargin);
15131
15151
  const rightAxisReserve = options.rightAxisReserve ?? 0;
15132
15152
  const provisionalArea = {
@@ -18006,11 +18026,15 @@ function compileMap(spec, options) {
18006
18026
  watermark
18007
18027
  );
18008
18028
  const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
18029
+ const chromeLayout = resolveChromeLayout(
18030
+ spec,
18031
+ options
18032
+ );
18009
18033
  const fullArea = {
18010
18034
  x: padding,
18011
18035
  y: padding + chrome.topHeight,
18012
18036
  width: options.width - padding * 2,
18013
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
18037
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
18014
18038
  };
18015
18039
  if (fullArea.width <= 0 || fullArea.height <= 0) {
18016
18040
  return emptyLayout3(chrome, theme, options, watermark);
@@ -18907,6 +18931,65 @@ import {
18907
18931
  resolveTheme as resolveTheme4
18908
18932
  } from "@opendata-ai/openchart-core";
18909
18933
 
18934
+ // src/graphs/animation.ts
18935
+ var ENTER_DEFAULT = {
18936
+ duration: 600,
18937
+ ease: "smooth",
18938
+ stagger: true,
18939
+ cameraFit: true
18940
+ };
18941
+ var UPDATE_DEFAULT = { duration: 300, ease: "smooth" };
18942
+ var EXIT_DEFAULT = { duration: 300, ease: "smooth" };
18943
+ var CAMERA_DEFAULT = { duration: "auto", ease: "smooth" };
18944
+ var HOVER_DEFAULT = { duration: 150, ease: "smooth" };
18945
+ function fullDefaults() {
18946
+ return {
18947
+ enter: { ...ENTER_DEFAULT },
18948
+ update: { ...UPDATE_DEFAULT },
18949
+ exit: { ...EXIT_DEFAULT },
18950
+ camera: { ...CAMERA_DEFAULT },
18951
+ hover: { ...HOVER_DEFAULT }
18952
+ };
18953
+ }
18954
+ function resolveGraphAnimation(spec) {
18955
+ if (spec === void 0 || spec === true) return fullDefaults();
18956
+ if (spec === false) return void 0;
18957
+ return {
18958
+ enter: resolveEnter(spec.enter),
18959
+ update: resolveDurationPhase(spec.update, UPDATE_DEFAULT),
18960
+ exit: resolveDurationPhase(spec.exit, EXIT_DEFAULT),
18961
+ camera: resolveCamera(spec.camera),
18962
+ hover: resolveDurationPhase(spec.hover, HOVER_DEFAULT)
18963
+ };
18964
+ }
18965
+ function resolveEnter(phase) {
18966
+ if (phase === false) return null;
18967
+ if (phase === void 0 || phase === true) return { ...ENTER_DEFAULT };
18968
+ return {
18969
+ duration: phase.duration ?? ENTER_DEFAULT.duration,
18970
+ ease: phase.ease ?? ENTER_DEFAULT.ease,
18971
+ stagger: phase.stagger !== false,
18972
+ cameraFit: ENTER_DEFAULT.cameraFit
18973
+ };
18974
+ }
18975
+ function resolveDurationPhase(phase, defaults) {
18976
+ if (phase === false) return null;
18977
+ if (phase === void 0 || phase === true)
18978
+ return { duration: defaults.duration, ease: defaults.ease };
18979
+ return {
18980
+ duration: phase.duration ?? defaults.duration,
18981
+ ease: phase.ease ?? defaults.ease
18982
+ };
18983
+ }
18984
+ function resolveCamera(phase) {
18985
+ if (phase === false) return null;
18986
+ if (phase === void 0 || phase === true) return { ...CAMERA_DEFAULT };
18987
+ return {
18988
+ duration: phase.duration ?? CAMERA_DEFAULT.duration,
18989
+ ease: phase.ease ?? CAMERA_DEFAULT.ease
18990
+ };
18991
+ }
18992
+
18910
18993
  // src/graphs/encoding.ts
18911
18994
  var DEFAULT_NODE_RADIUS = 5;
18912
18995
  var MIN_NODE_RADIUS = 3;
@@ -18946,16 +19029,42 @@ function computeDegrees(nodes, edges) {
18946
19029
  }
18947
19030
  return degrees2;
18948
19031
  }
19032
+ function resolveCategoricalDomain(values, sort, scaleDomain) {
19033
+ if (Array.isArray(scaleDomain) && scaleDomain.length > 0) {
19034
+ return scaleDomain.map((v) => String(v));
19035
+ }
19036
+ const seen = [];
19037
+ const set = /* @__PURE__ */ new Set();
19038
+ for (const v of values) {
19039
+ if (!set.has(v)) {
19040
+ set.add(v);
19041
+ seen.push(v);
19042
+ }
19043
+ }
19044
+ if (sort === null) return seen;
19045
+ if (Array.isArray(sort)) {
19046
+ const pinned = sort.filter((v) => set.has(v));
19047
+ const pinnedSet = new Set(pinned);
19048
+ const rest = seen.filter((v) => !pinnedSet.has(v));
19049
+ return [...pinned, ...rest];
19050
+ }
19051
+ const sorted = [...seen].sort((a, b) => a.localeCompare(b));
19052
+ return sort === "descending" ? sorted.reverse() : sorted;
19053
+ }
18949
19054
  function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18950
19055
  const degrees2 = computeDegrees(nodes, edges);
18951
19056
  const maxDegree = Math.max(1, ...degrees2.values());
18952
19057
  let sizeScale;
18953
19058
  if (encoding.nodeSize?.field) {
18954
19059
  const field = encoding.nodeSize.field;
19060
+ const scaleConfig = encoding.nodeSize.scale;
18955
19061
  const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
18956
19062
  const sizeMin = min(values) ?? 0;
18957
19063
  const sizeMax = max(values) ?? 1;
18958
- sizeScale = sqrt().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
19064
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [sizeMin, sizeMax];
19065
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [MIN_NODE_RADIUS, MAX_NODE_RADIUS];
19066
+ const ctor = scaleConfig?.type === "linear" ? linear2 : sqrt;
19067
+ sizeScale = ctor().domain(domain).range(range3);
18959
19068
  }
18960
19069
  let colorFn;
18961
19070
  if (encoding.nodeColor?.field) {
@@ -18976,13 +19085,28 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18976
19085
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
18977
19086
  };
18978
19087
  } else {
18979
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
19088
+ const domain = resolveCategoricalDomain(
19089
+ nodes.map((n) => String(n[field] ?? "")),
19090
+ encoding.nodeColor.sort,
19091
+ scaleConfig?.domain
19092
+ );
18980
19093
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18981
19094
  const ordinalScale = ordinal().domain(domain).range(range3);
18982
19095
  colorFn = (node) => ordinalScale(String(node[field] ?? ""));
18983
19096
  }
18984
19097
  }
18985
19098
  const defaultColor = theme.colors.categorical[0];
19099
+ let opacityScale;
19100
+ if (encoding.nodeOpacity?.field) {
19101
+ const field = encoding.nodeOpacity.field;
19102
+ const scaleConfig = encoding.nodeOpacity.scale;
19103
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19104
+ const opMin = min(values) ?? 0;
19105
+ const opMax = max(values) ?? 1;
19106
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [opMin, opMax];
19107
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0.25, 1];
19108
+ opacityScale = linear2().domain(domain).range(range3);
19109
+ }
18986
19110
  return nodes.map((node) => {
18987
19111
  let radius = DEFAULT_NODE_RADIUS;
18988
19112
  if (sizeScale && encoding.nodeSize?.field) {
@@ -18991,6 +19115,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18991
19115
  radius = sizeScale(val);
18992
19116
  }
18993
19117
  }
19118
+ let opacity = 1;
19119
+ if (opacityScale && encoding.nodeOpacity?.field) {
19120
+ const val = Number(node[encoding.nodeOpacity.field]);
19121
+ if (Number.isFinite(val)) {
19122
+ opacity = opacityScale(val);
19123
+ }
19124
+ }
18994
19125
  const fill = colorFn ? colorFn(node) : defaultColor;
18995
19126
  const stroke = darkenColor(fill);
18996
19127
  let label;
@@ -19019,6 +19150,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
19019
19150
  label,
19020
19151
  labelPriority: finalLabelPriority,
19021
19152
  community: void 0,
19153
+ opacity,
19022
19154
  data
19023
19155
  };
19024
19156
  });
@@ -19051,7 +19183,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19051
19183
  return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
19052
19184
  };
19053
19185
  } else {
19054
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
19186
+ const domain = resolveCategoricalDomain(
19187
+ edges.map((e) => String(e[field] ?? "")),
19188
+ encoding.edgeColor.sort,
19189
+ scaleConfig?.domain
19190
+ );
19055
19191
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
19056
19192
  const ordinalScale = ordinal().domain(domain).range(range3);
19057
19193
  edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
@@ -19062,10 +19198,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19062
19198
  let styleFn;
19063
19199
  if (encoding.edgeStyle?.field) {
19064
19200
  const field = encoding.edgeStyle.field;
19065
- const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
19201
+ const domain = resolveCategoricalDomain(
19202
+ edges.map((e) => String(e[field] ?? "")),
19203
+ encoding.edgeStyle.sort,
19204
+ encoding.edgeStyle.scale?.domain
19205
+ );
19066
19206
  const styleMap = /* @__PURE__ */ new Map();
19067
- for (let i = 0; i < uniqueValues.length; i++) {
19068
- styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19207
+ for (let i = 0; i < domain.length; i++) {
19208
+ styleMap.set(domain[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19069
19209
  }
19070
19210
  styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
19071
19211
  }
@@ -19123,12 +19263,49 @@ function applyCommunityColors(nodes, colorMap) {
19123
19263
  }
19124
19264
  }
19125
19265
 
19266
+ // src/graphs/interaction.ts
19267
+ var DEFAULT_DIM_OPACITY = 0.15;
19268
+ var DEFAULT_CURSOR_RADIUS = 80;
19269
+ var DEFAULT_CURSOR_STRENGTH = 30;
19270
+ function resolveGraphInteraction(cfg) {
19271
+ const hover = cfg?.hover;
19272
+ const cursor = cfg?.cursorRepulsion;
19273
+ let cursorRepulsion = null;
19274
+ if (cursor === true) {
19275
+ cursorRepulsion = { radius: DEFAULT_CURSOR_RADIUS, strength: DEFAULT_CURSOR_STRENGTH };
19276
+ } else if (cursor && typeof cursor === "object") {
19277
+ cursorRepulsion = {
19278
+ radius: cursor.radius ?? DEFAULT_CURSOR_RADIUS,
19279
+ strength: cursor.strength ?? DEFAULT_CURSOR_STRENGTH
19280
+ };
19281
+ }
19282
+ return {
19283
+ hoverMode: hover?.mode ?? "neighbors",
19284
+ dimOpacity: hover?.dimOpacity ?? DEFAULT_DIM_OPACITY,
19285
+ selectFlyTo: cfg?.select?.flyTo ?? false,
19286
+ cursorRepulsion,
19287
+ springyDrag: cfg?.springyDrag ?? false
19288
+ };
19289
+ }
19290
+
19126
19291
  // src/graphs/compile-graph.ts
19127
19292
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
19128
19293
  var SWATCH_SIZE2 = 12;
19129
19294
  var SWATCH_GAP2 = 6;
19130
19295
  var ENTRY_GAP2 = 16;
19131
- function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
19296
+ var ENERGY_PRESETS = {
19297
+ gentle: { chargeStrength: -150, velocityDecay: 0.5 },
19298
+ balanced: { chargeStrength: -300, velocityDecay: 0.4 },
19299
+ energetic: { chargeStrength: -600, velocityDecay: 0.3 }
19300
+ };
19301
+ var SETTLE_PRESETS = {
19302
+ quick: 0.05,
19303
+ balanced: 0.0228,
19304
+ thorough: 0.01
19305
+ };
19306
+ var DEFAULT_WARMUP_TICKS = 100;
19307
+ var DEFAULT_WARMUP_BUDGET_MS = 250;
19308
+ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField, legendOrder) {
19132
19309
  const labelStyle = {
19133
19310
  fontFamily: theme.fonts.family,
19134
19311
  fontSize: theme.fonts.sizes.small,
@@ -19138,25 +19315,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19138
19315
  };
19139
19316
  let entries;
19140
19317
  if (hasCommunities && communityColorMap.size > 0) {
19318
+ const counts = /* @__PURE__ */ new Map();
19319
+ for (const node of nodes) {
19320
+ if (node.community != null) {
19321
+ counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
19322
+ }
19323
+ }
19141
19324
  entries = [...communityColorMap.entries()].map(([label, color2]) => ({
19142
19325
  label,
19143
19326
  color: color2,
19144
19327
  shape: "circle",
19145
- active: true
19328
+ active: true,
19329
+ count: counts.get(label) ?? 0
19146
19330
  }));
19147
19331
  } else if (nodeColorField) {
19148
19332
  const categoryColors = /* @__PURE__ */ new Map();
19333
+ const categoryCounts = /* @__PURE__ */ new Map();
19149
19334
  for (const node of nodes) {
19150
19335
  const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
19151
19336
  if (!categoryColors.has(category)) {
19152
19337
  categoryColors.set(category, node.fill);
19153
19338
  }
19339
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19154
19340
  }
19155
- entries = categoryColors.size <= 1 ? [] : [...categoryColors.entries()].map(([label, color2]) => ({
19341
+ entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
19156
19342
  label,
19157
- color: color2,
19343
+ color: categoryColors.get(label),
19158
19344
  shape: "circle",
19159
- active: true
19345
+ active: true,
19346
+ count: categoryCounts.get(label) ?? 0
19160
19347
  }));
19161
19348
  } else {
19162
19349
  entries = [];
@@ -19172,6 +19359,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19172
19359
  swatchChipFill: theme.colors.annotationFill
19173
19360
  };
19174
19361
  }
19362
+ function buildEdgeLegend(edges, encoding) {
19363
+ const field = encoding.edgeColor?.field;
19364
+ if (!field || (encoding.edgeColor?.type ?? "nominal") === "quantitative") return void 0;
19365
+ const categoryColors = /* @__PURE__ */ new Map();
19366
+ const categoryCounts = /* @__PURE__ */ new Map();
19367
+ for (const edge of edges) {
19368
+ const category = String(edge.data[field] ?? "");
19369
+ if (!categoryColors.has(category)) categoryColors.set(category, edge.stroke);
19370
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19371
+ }
19372
+ if (categoryColors.size <= 1) return void 0;
19373
+ const order = resolveCategoricalDomain(
19374
+ edges.map((e) => String(e.data[field] ?? "")),
19375
+ encoding.edgeColor?.sort,
19376
+ encoding.edgeColor?.scale?.domain
19377
+ );
19378
+ return order.filter((label) => categoryColors.has(label)).map((label) => ({
19379
+ label,
19380
+ color: categoryColors.get(label),
19381
+ shape: "line",
19382
+ active: true,
19383
+ count: categoryCounts.get(label) ?? 0
19384
+ }));
19385
+ }
19175
19386
  function buildGraphTooltips(nodes) {
19176
19387
  const descriptors = /* @__PURE__ */ new Map();
19177
19388
  for (const node of nodes) {
@@ -19198,6 +19409,21 @@ function buildGraphTooltips(nodes) {
19198
19409
  }
19199
19410
  return descriptors;
19200
19411
  }
19412
+ function buildEdgeTooltip(edge) {
19413
+ const fields = [];
19414
+ for (const [key, value2] of Object.entries(edge.data)) {
19415
+ if (key === "source" || key === "target") continue;
19416
+ if (value2 == null) continue;
19417
+ fields.push({
19418
+ label: key,
19419
+ value: typeof value2 === "number" ? graphNumberFormatter(value2) : String(value2)
19420
+ });
19421
+ }
19422
+ return {
19423
+ title: `${edge.source} \u2192 ${edge.target}`,
19424
+ fields
19425
+ };
19426
+ }
19201
19427
  function compileGraph(spec, options) {
19202
19428
  const { spec: normalized, warnings } = compile(spec);
19203
19429
  emitSpecWarnings(warnings, options.onWarn);
@@ -19207,6 +19433,16 @@ function compileGraph(spec, options) {
19207
19433
  );
19208
19434
  }
19209
19435
  const graphSpec = normalized;
19436
+ for (const [name, ch] of [
19437
+ ["nodeColor", graphSpec.encoding.nodeColor],
19438
+ ["edgeColor", graphSpec.encoding.edgeColor]
19439
+ ]) {
19440
+ if (ch?.sort != null && ch.type === "quantitative") {
19441
+ options.onWarn?.(
19442
+ `encoding.${name}.sort is ignored for a quantitative field (sort only orders categorical domains).`
19443
+ );
19444
+ }
19445
+ }
19210
19446
  const rawWatermark = spec.watermark;
19211
19447
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
19212
19448
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
@@ -19232,13 +19468,30 @@ function compileGraph(spec, options) {
19232
19468
  }
19233
19469
  const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
19234
19470
  const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
19471
+ const nodeColorField = graphSpec.encoding.nodeColor?.field;
19472
+ const legendOrder = nodeColorField ? resolveCategoricalDomain(
19473
+ compiledNodes.map((n) => String(n.data[nodeColorField] ?? n.label ?? n.id)),
19474
+ graphSpec.encoding.nodeColor?.sort,
19475
+ graphSpec.encoding.nodeColor?.scale?.domain
19476
+ ) : void 0;
19235
19477
  const legend = buildGraphLegend(
19236
19478
  compiledNodes,
19237
19479
  communityColorMap,
19238
19480
  useCommunitiesForLegend,
19239
19481
  theme,
19240
- graphSpec.encoding.nodeColor?.field
19482
+ nodeColorField,
19483
+ legendOrder
19241
19484
  );
19485
+ const edgeLegend = buildEdgeLegend(compiledEdges, graphSpec.encoding);
19486
+ const legendField = nodeColorField ?? (useCommunitiesForLegend ? clusteringField ?? null : null);
19487
+ let initialHighlight;
19488
+ if (nodeColorField && graphSpec.encoding.nodeColor?.highlight != null) {
19489
+ const raw = graphSpec.encoding.nodeColor.highlight;
19490
+ const requested = Array.isArray(raw) ? raw : [raw];
19491
+ const available = new Set(legendOrder ?? []);
19492
+ const values = requested.filter((v) => available.has(v));
19493
+ if (values.length > 0) initialHighlight = { field: nodeColorField, values };
19494
+ }
19242
19495
  const tooltipDescriptors = buildGraphTooltips(compiledNodes);
19243
19496
  const communityCount = communityColorMap.size;
19244
19497
  const altParts = [
@@ -19255,17 +19508,26 @@ function compileGraph(spec, options) {
19255
19508
  };
19256
19509
  const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
19257
19510
  const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
19511
+ const energyPreset = graphSpec.layout.energy ? ENERGY_PRESETS[graphSpec.layout.energy] : ENERGY_PRESETS.balanced;
19512
+ const settlePreset = graphSpec.layout.settle ? SETTLE_PRESETS[graphSpec.layout.settle] : SETTLE_PRESETS.balanced;
19513
+ const warmupRaw = graphSpec.layout.warmup;
19514
+ const warmupTicks = warmupRaw === false ? 0 : typeof warmupRaw === "number" ? Math.max(0, Math.floor(warmupRaw)) : DEFAULT_WARMUP_TICKS;
19258
19515
  const simulationConfig = {
19259
- chargeStrength: graphSpec.layout.chargeStrength ?? -300,
19516
+ chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
19260
19517
  linkDistance: graphSpec.layout.linkDistance ?? 30,
19261
19518
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
19262
- alphaDecay: 0.0228,
19263
- velocityDecay: 0.4,
19519
+ alphaDecay: settlePreset,
19520
+ velocityDecay: energyPreset.velocityDecay,
19264
19521
  collisionRadius: maxRadius + collisionPadding,
19265
19522
  collisionPadding,
19266
19523
  linkStrength: graphSpec.layout.linkStrength,
19267
- centerForce: graphSpec.layout.centerForce
19524
+ centerForce: graphSpec.layout.centerForce,
19525
+ seed: graphSpec.layout.seed,
19526
+ warmupTicks,
19527
+ warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
19268
19528
  };
19529
+ const animation = resolveGraphAnimation(graphSpec.animation);
19530
+ const interaction = resolveGraphInteraction(graphSpec.interaction);
19269
19531
  const chrome = computeChrome4(
19270
19532
  {
19271
19533
  title: graphSpec.chrome.title,
@@ -19294,7 +19556,12 @@ function compileGraph(spec, options) {
19294
19556
  height: options.height
19295
19557
  },
19296
19558
  simulationConfig,
19297
- watermark
19559
+ watermark,
19560
+ animation,
19561
+ interaction,
19562
+ legendField,
19563
+ initialHighlight,
19564
+ edgeLegend
19298
19565
  };
19299
19566
  }
19300
19567
  var DEFAULT_COLLISION_PADDING = 5;
@@ -19387,6 +19654,7 @@ function legendLayoutHasContent(legendLayout) {
19387
19654
  }
19388
19655
  var MIN_CHART_WIDTH = 60;
19389
19656
  var MIN_CHART_HEIGHT2 = 40;
19657
+ var PLOT_SHARE_MAX = 0.4;
19390
19658
  var SERIES_SEARCH_INPUT_HEIGHT = 32;
19391
19659
  var SERIES_SEARCH_GAP = 12;
19392
19660
  function getMinChartDims(display) {
@@ -19429,8 +19697,10 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19429
19697
  watermark,
19430
19698
  bottomLegendReservation
19431
19699
  );
19700
+ const chromeLayout = resolveChromeLayout(spec, options);
19701
+ const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
19432
19702
  if (isSparkline) {
19433
- const total2 = { x: 0, y: 0, width, height };
19703
+ const total2 = { x: 0, y: 0, width, height: effectiveHeight };
19434
19704
  const sparkPad = getSparklinePad(spec);
19435
19705
  const xAxisSpace = userExplicit.xAxis ? 26 : 0;
19436
19706
  const yAxisSpace = userExplicit.yAxis ? 30 : 0;
@@ -19454,7 +19724,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19454
19724
  x: margins2.left,
19455
19725
  y: margins2.top,
19456
19726
  width: Math.max(0, width - margins2.left - margins2.right),
19457
- height: Math.max(0, height - margins2.top - margins2.bottom)
19727
+ height: Math.max(0, effectiveHeight - margins2.top - margins2.bottom)
19458
19728
  };
19459
19729
  return {
19460
19730
  total: total2,
@@ -19466,7 +19736,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19466
19736
  effectiveAxisGap: 0
19467
19737
  };
19468
19738
  }
19469
- const total = { x: 0, y: 0, width, height };
19739
+ const total = { x: 0, y: 0, width, height: effectiveHeight };
19470
19740
  const isRadial = isAxislessMark2(spec.markType);
19471
19741
  const encoding = spec.encoding;
19472
19742
  const xAxisSuppressed = encoding.x?.axis === false;
@@ -19696,10 +19966,11 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19696
19966
  x: margins.left,
19697
19967
  y: margins.top,
19698
19968
  width: Math.max(0, width - margins.left - margins.right),
19699
- height: Math.max(0, height - margins.top - margins.bottom)
19969
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19700
19970
  };
19701
19971
  const minDims = getMinChartDims(spec.display);
19702
- if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
19972
+ const chromeEatsPlot = chromeLayout !== "grow" && chrome.topHeight > height * PLOT_SHARE_MAX;
19973
+ if ((chartArea.width < minDims.width || chartArea.height < minDims.height || chromeEatsPlot) && chromeMode !== "hidden") {
19703
19974
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
19704
19975
  const fallbackChrome = computeChrome5(
19705
19976
  chromeToInput(spec.chrome),
@@ -19725,7 +19996,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19725
19996
  x: margins.left,
19726
19997
  y: margins.top,
19727
19998
  width: Math.max(0, width - margins.left - margins.right),
19728
- height: Math.max(0, height - margins.top - margins.bottom)
19999
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19729
20000
  };
19730
20001
  const fallbackMetricsTopY = topPad + fallbackChrome.topHeight;
19731
20002
  const fallbackMetricsArea = { x: hPad, width: Math.max(0, width - hPad * 2) };
@@ -19742,7 +20013,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19742
20013
  chartArea = {
19743
20014
  ...chartArea,
19744
20015
  y: margins.top,
19745
- height: Math.max(0, height - margins.top - margins.bottom)
20016
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19746
20017
  };
19747
20018
  }
19748
20019
  return {
@@ -19758,7 +20029,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19758
20029
  fallbackMetricsArea
19759
20030
  ) : void 0,
19760
20031
  xAxisHeight,
19761
- effectiveAxisGap: fallbackEffectiveAxisGap
20032
+ effectiveAxisGap: fallbackEffectiveAxisGap,
20033
+ chromeStripped: true
19762
20034
  };
19763
20035
  }
19764
20036
  }
@@ -19770,7 +20042,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
19770
20042
  chartArea = {
19771
20043
  ...chartArea,
19772
20044
  y: margins.top,
19773
- height: Math.max(0, height - margins.top - margins.bottom)
20045
+ height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
19774
20046
  };
19775
20047
  }
19776
20048
  return {
@@ -20522,14 +20794,19 @@ function compileSankey(spec, options) {
20522
20794
  watermark
20523
20795
  );
20524
20796
  const padding = theme.spacing.padding;
20797
+ const chromeLayout = resolveChromeLayout(
20798
+ spec,
20799
+ options
20800
+ );
20801
+ const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
20525
20802
  const fullArea = {
20526
20803
  x: padding,
20527
20804
  y: padding + chrome.topHeight,
20528
20805
  width: options.width - padding * 2,
20529
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
20806
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
20530
20807
  };
20531
20808
  if (fullArea.width <= 0 || fullArea.height <= 0) {
20532
- return emptyLayout4(fullArea, chrome, theme, options, watermark);
20809
+ return emptyLayout4(fullArea, chrome, theme, { ...options, height: grownHeight }, watermark);
20533
20810
  }
20534
20811
  const sourceField = sankeySpec.encoding.source.field;
20535
20812
  const targetField = sankeySpec.encoding.target.field;
@@ -20569,7 +20846,7 @@ function compileSankey(spec, options) {
20569
20846
  height: fullArea.height - legend.bounds.height - legendGap2
20570
20847
  };
20571
20848
  if (area.height <= 0) {
20572
- return emptyLayout4(area, chrome, theme, options, watermark);
20849
+ return emptyLayout4(area, chrome, theme, { ...options, height: grownHeight }, watermark);
20573
20850
  }
20574
20851
  const labelFontSize = theme.fonts.sizes.small;
20575
20852
  const labelFontWeight = theme.fonts.weights.normal;
@@ -20721,7 +20998,7 @@ function compileSankey(spec, options) {
20721
20998
  theme,
20722
20999
  dimensions: {
20723
21000
  width: options.width,
20724
- height: options.height
21001
+ height: grownHeight
20725
21002
  },
20726
21003
  animation: resolvedAnimation,
20727
21004
  watermark,
@@ -21540,6 +21817,10 @@ var LEGEND_ENTRY_GAP = 16;
21540
21817
  function clamp(value2, min4, max4) {
21541
21818
  return Math.min(max4, Math.max(min4, value2));
21542
21819
  }
21820
+ function growContentHeight(contentHeight, chromeLayout, budgetHeight, chrome) {
21821
+ if (chromeLayout !== "grow") return contentHeight;
21822
+ return Math.max(contentHeight, budgetHeight + chrome.topHeight + chrome.bottomHeight);
21823
+ }
21543
21824
  function compileTileMap(spec, options) {
21544
21825
  const sugarWarnings = [];
21545
21826
  const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
@@ -21576,22 +21857,44 @@ function compileTileMap(spec, options) {
21576
21857
  watermark
21577
21858
  );
21578
21859
  const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
21860
+ const chromeLayout = resolveChromeLayout(
21861
+ spec,
21862
+ options
21863
+ );
21579
21864
  const fullArea = {
21580
21865
  x: padding,
21581
21866
  y: padding + chrome.topHeight,
21582
21867
  width: options.width - padding * 2,
21583
- height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
21868
+ height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
21584
21869
  };
21585
21870
  if (fullArea.width <= 0 || fullArea.height <= 0) {
21586
21871
  return emptyLayout5(chrome, theme, options, watermark);
21587
21872
  }
21588
21873
  const isCategorical = tilemapSpec.encoding.color !== void 0 || tilemapSpec.colors !== void 0;
21589
21874
  if (isCategorical) {
21590
- return compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
21875
+ return compileCategorical(
21876
+ tilemapSpec,
21877
+ options,
21878
+ theme,
21879
+ chrome,
21880
+ fullArea,
21881
+ isDarkMode,
21882
+ watermark,
21883
+ chromeLayout
21884
+ );
21591
21885
  }
21592
- return compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
21886
+ return compileQuantitative(
21887
+ tilemapSpec,
21888
+ options,
21889
+ theme,
21890
+ chrome,
21891
+ fullArea,
21892
+ isDarkMode,
21893
+ watermark,
21894
+ chromeLayout
21895
+ );
21593
21896
  }
21594
- function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
21897
+ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21595
21898
  const stateField = tilemapSpec.encoding.state.field;
21596
21899
  const valueField = tilemapSpec.encoding.value.field;
21597
21900
  const stateValueMap = /* @__PURE__ */ new Map();
@@ -21692,7 +21995,12 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21692
21995
  };
21693
21996
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21694
21997
  const padding = fullArea.x;
21695
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
21998
+ const contentHeight = growContentHeight(
21999
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22000
+ chromeLayout,
22001
+ options.height,
22002
+ chrome
22003
+ );
21696
22004
  return {
21697
22005
  area: fullArea,
21698
22006
  chrome,
@@ -21709,7 +22017,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
21709
22017
  measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth23(text, fontSize), height: fontSize }))
21710
22018
  };
21711
22019
  }
21712
- function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
22020
+ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
21713
22021
  const stateField = tilemapSpec.encoding.state.field;
21714
22022
  const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
21715
22023
  const stateCategoryMap = /* @__PURE__ */ new Map();
@@ -21828,7 +22136,12 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
21828
22136
  };
21829
22137
  const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
21830
22138
  const padding = fullArea.x;
21831
- const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
22139
+ const contentHeight = growContentHeight(
22140
+ tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
22141
+ chromeLayout,
22142
+ options.height,
22143
+ chrome
22144
+ );
21832
22145
  return {
21833
22146
  area: fullArea,
21834
22147
  chrome,
@@ -23213,7 +23526,10 @@ function compileChart(spec, optionsInput) {
23213
23526
  skipY,
23214
23527
  markType: chartSpec.markType,
23215
23528
  totalWidth: options.width,
23216
- precomputedYTicks: plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23529
+ // If the guardrail stripped chrome, the plan's y-ticks were sized
23530
+ // against the pre-strip (shorter) plot. Drop them so computeAxes
23531
+ // regenerates against the final (taller) chartArea.
23532
+ precomputedYTicks: !dims.chromeStripped && plan.yTickValues.length > 0 ? plan.yTickValues : void 0
23217
23533
  });
23218
23534
  const xAxisExtent = axes.x?.extent ?? 0;
23219
23535
  dims.chrome.bottomAnchorY = chartArea.y + chartArea.height + xAxisExtent;
@@ -23336,7 +23652,10 @@ function compileChart(spec, optionsInput) {
23336
23652
  theme,
23337
23653
  dimensions: {
23338
23654
  width: options.width,
23339
- height: options.height
23655
+ // In chromeLayout 'grow' mode, computeDimensions grows total.height by the
23656
+ // chrome height so the plot keeps its full budget. In the default
23657
+ // 'subtract' mode total.height === options.height, so this is a no-op.
23658
+ height: dims.total.height
23340
23659
  },
23341
23660
  animation: resolvedAnimation,
23342
23661
  watermark,
@@ -23669,6 +23988,7 @@ export {
23669
23988
  EXIT_DEFAULTS,
23670
23989
  UPDATE_DEFAULTS,
23671
23990
  buildAreaPath,
23991
+ buildEdgeTooltip,
23672
23992
  buildLinePath,
23673
23993
  clampStaggerDelay,
23674
23994
  clearRenderers,