@opendata-ai/openchart-engine 8.0.0-rc.12 → 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
@@ -10557,15 +10557,10 @@ function normalizeSankeySpec(spec, _warnings) {
10557
10557
  };
10558
10558
  }
10559
10559
  function normalizeGraphSpec(spec) {
10560
- const defaultLayout = {
10560
+ const layout = {
10561
10561
  type: "force",
10562
- chargeStrength: -300,
10563
- linkDistance: 30
10564
- };
10565
- const layout = spec.layout ? {
10566
- ...defaultLayout,
10567
10562
  ...spec.layout
10568
- } : defaultLayout;
10563
+ };
10569
10564
  return {
10570
10565
  type: "graph",
10571
10566
  nodes: spec.nodes,
@@ -10577,7 +10572,10 @@ function normalizeGraphSpec(spec) {
10577
10572
  annotations: normalizeAnnotations(spec.annotations),
10578
10573
  theme: spec.theme ?? {},
10579
10574
  darkMode: spec.darkMode ?? "off",
10580
- watermark: spec.watermark ?? true
10575
+ watermark: spec.watermark ?? true,
10576
+ animation: spec.animation,
10577
+ interaction: spec.interaction,
10578
+ legend: spec.legend
10581
10579
  };
10582
10580
  }
10583
10581
  function normalizeTileMapSpec(spec, warnings) {
@@ -11606,14 +11604,22 @@ function validateGraphSpec(spec, errors) {
11606
11604
  }
11607
11605
  if (spec.encoding && typeof spec.encoding === "object") {
11608
11606
  const encoding = spec.encoding;
11609
- const firstNode = nodes[0];
11610
- const firstEdge = edges.length > 0 ? edges[0] : null;
11611
- const nodeFields = firstNode ? new Set(Object.keys(firstNode)) : /* @__PURE__ */ new Set();
11612
- const edgeFields = firstEdge ? new Set(Object.keys(firstEdge)) : /* @__PURE__ */ new Set();
11613
- 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"];
11614
11620
  for (const channel of nodeChannels) {
11615
11621
  const ch = encoding[channel];
11616
- 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)) {
11617
11623
  errors.push({
11618
11624
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
11619
11625
  path: `encoding.${channel}.field`,
@@ -11622,10 +11628,10 @@ function validateGraphSpec(spec, errors) {
11622
11628
  });
11623
11629
  }
11624
11630
  }
11625
- const edgeChannels = ["edgeColor", "edgeWidth"];
11631
+ const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
11626
11632
  for (const channel of edgeChannels) {
11627
11633
  const ch = encoding[channel];
11628
- 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)) {
11629
11635
  errors.push({
11630
11636
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
11631
11637
  path: `encoding.${channel}.field`,
@@ -18925,6 +18931,65 @@ import {
18925
18931
  resolveTheme as resolveTheme4
18926
18932
  } from "@opendata-ai/openchart-core";
18927
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
+
18928
18993
  // src/graphs/encoding.ts
18929
18994
  var DEFAULT_NODE_RADIUS = 5;
18930
18995
  var MIN_NODE_RADIUS = 3;
@@ -18964,16 +19029,42 @@ function computeDegrees(nodes, edges) {
18964
19029
  }
18965
19030
  return degrees2;
18966
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
+ }
18967
19054
  function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18968
19055
  const degrees2 = computeDegrees(nodes, edges);
18969
19056
  const maxDegree = Math.max(1, ...degrees2.values());
18970
19057
  let sizeScale;
18971
19058
  if (encoding.nodeSize?.field) {
18972
19059
  const field = encoding.nodeSize.field;
19060
+ const scaleConfig = encoding.nodeSize.scale;
18973
19061
  const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
18974
19062
  const sizeMin = min(values) ?? 0;
18975
19063
  const sizeMax = max(values) ?? 1;
18976
- 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);
18977
19068
  }
18978
19069
  let colorFn;
18979
19070
  if (encoding.nodeColor?.field) {
@@ -18994,13 +19085,28 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18994
19085
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
18995
19086
  };
18996
19087
  } else {
18997
- 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
+ );
18998
19093
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18999
19094
  const ordinalScale = ordinal().domain(domain).range(range3);
19000
19095
  colorFn = (node) => ordinalScale(String(node[field] ?? ""));
19001
19096
  }
19002
19097
  }
19003
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
+ }
19004
19110
  return nodes.map((node) => {
19005
19111
  let radius = DEFAULT_NODE_RADIUS;
19006
19112
  if (sizeScale && encoding.nodeSize?.field) {
@@ -19009,6 +19115,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
19009
19115
  radius = sizeScale(val);
19010
19116
  }
19011
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
+ }
19012
19125
  const fill = colorFn ? colorFn(node) : defaultColor;
19013
19126
  const stroke = darkenColor(fill);
19014
19127
  let label;
@@ -19037,6 +19150,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
19037
19150
  label,
19038
19151
  labelPriority: finalLabelPriority,
19039
19152
  community: void 0,
19153
+ opacity,
19040
19154
  data
19041
19155
  };
19042
19156
  });
@@ -19069,7 +19183,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19069
19183
  return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
19070
19184
  };
19071
19185
  } else {
19072
- 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
+ );
19073
19191
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
19074
19192
  const ordinalScale = ordinal().domain(domain).range(range3);
19075
19193
  edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
@@ -19080,10 +19198,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19080
19198
  let styleFn;
19081
19199
  if (encoding.edgeStyle?.field) {
19082
19200
  const field = encoding.edgeStyle.field;
19083
- 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
+ );
19084
19206
  const styleMap = /* @__PURE__ */ new Map();
19085
- for (let i = 0; i < uniqueValues.length; i++) {
19086
- 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]);
19087
19209
  }
19088
19210
  styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
19089
19211
  }
@@ -19141,12 +19263,49 @@ function applyCommunityColors(nodes, colorMap) {
19141
19263
  }
19142
19264
  }
19143
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
+
19144
19291
  // src/graphs/compile-graph.ts
19145
19292
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
19146
19293
  var SWATCH_SIZE2 = 12;
19147
19294
  var SWATCH_GAP2 = 6;
19148
19295
  var ENTRY_GAP2 = 16;
19149
- 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) {
19150
19309
  const labelStyle = {
19151
19310
  fontFamily: theme.fonts.family,
19152
19311
  fontSize: theme.fonts.sizes.small,
@@ -19156,25 +19315,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19156
19315
  };
19157
19316
  let entries;
19158
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
+ }
19159
19324
  entries = [...communityColorMap.entries()].map(([label, color2]) => ({
19160
19325
  label,
19161
19326
  color: color2,
19162
19327
  shape: "circle",
19163
- active: true
19328
+ active: true,
19329
+ count: counts.get(label) ?? 0
19164
19330
  }));
19165
19331
  } else if (nodeColorField) {
19166
19332
  const categoryColors = /* @__PURE__ */ new Map();
19333
+ const categoryCounts = /* @__PURE__ */ new Map();
19167
19334
  for (const node of nodes) {
19168
19335
  const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
19169
19336
  if (!categoryColors.has(category)) {
19170
19337
  categoryColors.set(category, node.fill);
19171
19338
  }
19339
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19172
19340
  }
19173
- entries = categoryColors.size <= 1 ? [] : [...categoryColors.entries()].map(([label, color2]) => ({
19341
+ entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
19174
19342
  label,
19175
- color: color2,
19343
+ color: categoryColors.get(label),
19176
19344
  shape: "circle",
19177
- active: true
19345
+ active: true,
19346
+ count: categoryCounts.get(label) ?? 0
19178
19347
  }));
19179
19348
  } else {
19180
19349
  entries = [];
@@ -19190,6 +19359,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19190
19359
  swatchChipFill: theme.colors.annotationFill
19191
19360
  };
19192
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
+ }
19193
19386
  function buildGraphTooltips(nodes) {
19194
19387
  const descriptors = /* @__PURE__ */ new Map();
19195
19388
  for (const node of nodes) {
@@ -19216,6 +19409,21 @@ function buildGraphTooltips(nodes) {
19216
19409
  }
19217
19410
  return descriptors;
19218
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
+ }
19219
19427
  function compileGraph(spec, options) {
19220
19428
  const { spec: normalized, warnings } = compile(spec);
19221
19429
  emitSpecWarnings(warnings, options.onWarn);
@@ -19225,6 +19433,16 @@ function compileGraph(spec, options) {
19225
19433
  );
19226
19434
  }
19227
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
+ }
19228
19446
  const rawWatermark = spec.watermark;
19229
19447
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
19230
19448
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
@@ -19250,13 +19468,30 @@ function compileGraph(spec, options) {
19250
19468
  }
19251
19469
  const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
19252
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;
19253
19477
  const legend = buildGraphLegend(
19254
19478
  compiledNodes,
19255
19479
  communityColorMap,
19256
19480
  useCommunitiesForLegend,
19257
19481
  theme,
19258
- graphSpec.encoding.nodeColor?.field
19482
+ nodeColorField,
19483
+ legendOrder
19259
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
+ }
19260
19495
  const tooltipDescriptors = buildGraphTooltips(compiledNodes);
19261
19496
  const communityCount = communityColorMap.size;
19262
19497
  const altParts = [
@@ -19273,17 +19508,26 @@ function compileGraph(spec, options) {
19273
19508
  };
19274
19509
  const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
19275
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;
19276
19515
  const simulationConfig = {
19277
- chargeStrength: graphSpec.layout.chargeStrength ?? -300,
19516
+ chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
19278
19517
  linkDistance: graphSpec.layout.linkDistance ?? 30,
19279
19518
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
19280
- alphaDecay: 0.0228,
19281
- velocityDecay: 0.4,
19519
+ alphaDecay: settlePreset,
19520
+ velocityDecay: energyPreset.velocityDecay,
19282
19521
  collisionRadius: maxRadius + collisionPadding,
19283
19522
  collisionPadding,
19284
19523
  linkStrength: graphSpec.layout.linkStrength,
19285
- centerForce: graphSpec.layout.centerForce
19524
+ centerForce: graphSpec.layout.centerForce,
19525
+ seed: graphSpec.layout.seed,
19526
+ warmupTicks,
19527
+ warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
19286
19528
  };
19529
+ const animation = resolveGraphAnimation(graphSpec.animation);
19530
+ const interaction = resolveGraphInteraction(graphSpec.interaction);
19287
19531
  const chrome = computeChrome4(
19288
19532
  {
19289
19533
  title: graphSpec.chrome.title,
@@ -19312,7 +19556,12 @@ function compileGraph(spec, options) {
19312
19556
  height: options.height
19313
19557
  },
19314
19558
  simulationConfig,
19315
- watermark
19559
+ watermark,
19560
+ animation,
19561
+ interaction,
19562
+ legendField,
19563
+ initialHighlight,
19564
+ edgeLegend
19316
19565
  };
19317
19566
  }
19318
19567
  var DEFAULT_COLLISION_PADDING = 5;
@@ -23739,6 +23988,7 @@ export {
23739
23988
  EXIT_DEFAULTS,
23740
23989
  UPDATE_DEFAULTS,
23741
23990
  buildAreaPath,
23991
+ buildEdgeTooltip,
23742
23992
  buildLinePath,
23743
23993
  clampStaggerDelay,
23744
23994
  clearRenderers,