@opendata-ai/openchart-engine 8.0.0-rc.12 → 8.0.0-rc.14

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,28 @@ 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 = [
11620
+ "nodeColor",
11621
+ "nodeSize",
11622
+ "nodeOpacity",
11623
+ "nodeLabel",
11624
+ "nodeLabelPriority"
11625
+ ];
11614
11626
  for (const channel of nodeChannels) {
11615
11627
  const ch = encoding[channel];
11616
- if (ch?.field && typeof ch.field === "string" && !nodeFields.has(ch.field)) {
11628
+ if (ch?.field && typeof ch.field === "string" && nodes.length > 0 && !nodeFields.has(ch.field)) {
11617
11629
  errors.push({
11618
11630
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
11619
11631
  path: `encoding.${channel}.field`,
@@ -11622,10 +11634,10 @@ function validateGraphSpec(spec, errors) {
11622
11634
  });
11623
11635
  }
11624
11636
  }
11625
- const edgeChannels = ["edgeColor", "edgeWidth"];
11637
+ const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
11626
11638
  for (const channel of edgeChannels) {
11627
11639
  const ch = encoding[channel];
11628
- if (ch?.field && typeof ch.field === "string" && firstEdge && !edgeFields.has(ch.field)) {
11640
+ if (ch?.field && typeof ch.field === "string" && edges.length > 0 && !edgeFields.has(ch.field)) {
11629
11641
  errors.push({
11630
11642
  message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
11631
11643
  path: `encoding.${channel}.field`,
@@ -18925,6 +18937,65 @@ import {
18925
18937
  resolveTheme as resolveTheme4
18926
18938
  } from "@opendata-ai/openchart-core";
18927
18939
 
18940
+ // src/graphs/animation.ts
18941
+ var ENTER_DEFAULT = {
18942
+ duration: 500,
18943
+ ease: "smooth",
18944
+ stagger: true,
18945
+ cameraFit: true
18946
+ };
18947
+ var UPDATE_DEFAULT = { duration: 300, ease: "smooth" };
18948
+ var EXIT_DEFAULT = { duration: 300, ease: "smooth" };
18949
+ var CAMERA_DEFAULT = { duration: "auto", ease: "smooth" };
18950
+ var HOVER_DEFAULT = { duration: 150, ease: "smooth" };
18951
+ function fullDefaults() {
18952
+ return {
18953
+ enter: { ...ENTER_DEFAULT },
18954
+ update: { ...UPDATE_DEFAULT },
18955
+ exit: { ...EXIT_DEFAULT },
18956
+ camera: { ...CAMERA_DEFAULT },
18957
+ hover: { ...HOVER_DEFAULT }
18958
+ };
18959
+ }
18960
+ function resolveGraphAnimation(spec) {
18961
+ if (spec === void 0 || spec === true) return fullDefaults();
18962
+ if (spec === false) return void 0;
18963
+ return {
18964
+ enter: resolveEnter(spec.enter),
18965
+ update: resolveDurationPhase(spec.update, UPDATE_DEFAULT),
18966
+ exit: resolveDurationPhase(spec.exit, EXIT_DEFAULT),
18967
+ camera: resolveCamera(spec.camera),
18968
+ hover: resolveDurationPhase(spec.hover, HOVER_DEFAULT)
18969
+ };
18970
+ }
18971
+ function resolveEnter(phase) {
18972
+ if (phase === false) return null;
18973
+ if (phase === void 0 || phase === true) return { ...ENTER_DEFAULT };
18974
+ return {
18975
+ duration: phase.duration ?? ENTER_DEFAULT.duration,
18976
+ ease: phase.ease ?? ENTER_DEFAULT.ease,
18977
+ stagger: phase.stagger !== false,
18978
+ cameraFit: ENTER_DEFAULT.cameraFit
18979
+ };
18980
+ }
18981
+ function resolveDurationPhase(phase, defaults) {
18982
+ if (phase === false) return null;
18983
+ if (phase === void 0 || phase === true)
18984
+ return { duration: defaults.duration, ease: defaults.ease };
18985
+ return {
18986
+ duration: phase.duration ?? defaults.duration,
18987
+ ease: phase.ease ?? defaults.ease
18988
+ };
18989
+ }
18990
+ function resolveCamera(phase) {
18991
+ if (phase === false) return null;
18992
+ if (phase === void 0 || phase === true) return { ...CAMERA_DEFAULT };
18993
+ return {
18994
+ duration: phase.duration ?? CAMERA_DEFAULT.duration,
18995
+ ease: phase.ease ?? CAMERA_DEFAULT.ease
18996
+ };
18997
+ }
18998
+
18928
18999
  // src/graphs/encoding.ts
18929
19000
  var DEFAULT_NODE_RADIUS = 5;
18930
19001
  var MIN_NODE_RADIUS = 3;
@@ -18964,16 +19035,42 @@ function computeDegrees(nodes, edges) {
18964
19035
  }
18965
19036
  return degrees2;
18966
19037
  }
19038
+ function resolveCategoricalDomain(values, sort, scaleDomain) {
19039
+ if (Array.isArray(scaleDomain) && scaleDomain.length > 0) {
19040
+ return scaleDomain.map((v) => String(v));
19041
+ }
19042
+ const seen = [];
19043
+ const set = /* @__PURE__ */ new Set();
19044
+ for (const v of values) {
19045
+ if (!set.has(v)) {
19046
+ set.add(v);
19047
+ seen.push(v);
19048
+ }
19049
+ }
19050
+ if (sort === null) return seen;
19051
+ if (Array.isArray(sort)) {
19052
+ const pinned = sort.filter((v) => set.has(v));
19053
+ const pinnedSet = new Set(pinned);
19054
+ const rest = seen.filter((v) => !pinnedSet.has(v));
19055
+ return [...pinned, ...rest];
19056
+ }
19057
+ const sorted = [...seen].sort((a, b) => a.localeCompare(b));
19058
+ return sort === "descending" ? sorted.reverse() : sorted;
19059
+ }
18967
19060
  function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18968
19061
  const degrees2 = computeDegrees(nodes, edges);
18969
19062
  const maxDegree = Math.max(1, ...degrees2.values());
18970
19063
  let sizeScale;
18971
19064
  if (encoding.nodeSize?.field) {
18972
19065
  const field = encoding.nodeSize.field;
19066
+ const scaleConfig = encoding.nodeSize.scale;
18973
19067
  const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
18974
19068
  const sizeMin = min(values) ?? 0;
18975
19069
  const sizeMax = max(values) ?? 1;
18976
- sizeScale = sqrt().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
19070
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [sizeMin, sizeMax];
19071
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [MIN_NODE_RADIUS, MAX_NODE_RADIUS];
19072
+ const ctor = scaleConfig?.type === "linear" ? linear2 : sqrt;
19073
+ sizeScale = ctor().domain(domain).range(range3);
18977
19074
  }
18978
19075
  let colorFn;
18979
19076
  if (encoding.nodeColor?.field) {
@@ -18994,13 +19091,39 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
18994
19091
  return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
18995
19092
  };
18996
19093
  } else {
18997
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
19094
+ const domain = resolveCategoricalDomain(
19095
+ nodes.map((n) => String(n[field] ?? "")),
19096
+ encoding.nodeColor.sort,
19097
+ scaleConfig?.domain
19098
+ );
18998
19099
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
18999
19100
  const ordinalScale = ordinal().domain(domain).range(range3);
19000
19101
  colorFn = (node) => ordinalScale(String(node[field] ?? ""));
19001
19102
  }
19002
19103
  }
19003
19104
  const defaultColor = theme.colors.categorical[0];
19105
+ let opacityScale;
19106
+ if (encoding.nodeOpacity?.field) {
19107
+ const field = encoding.nodeOpacity.field;
19108
+ const scaleConfig = encoding.nodeOpacity.scale;
19109
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19110
+ const opMin = min(values) ?? 0;
19111
+ const opMax = max(values) ?? 1;
19112
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [opMin, opMax];
19113
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0.25, 1];
19114
+ opacityScale = linear2().domain(domain).range(range3);
19115
+ }
19116
+ let labelPriorityScale;
19117
+ if (encoding.nodeLabelPriority?.field) {
19118
+ const field = encoding.nodeLabelPriority.field;
19119
+ const scaleConfig = encoding.nodeLabelPriority.scale;
19120
+ const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
19121
+ const lpMin = min(values) ?? 0;
19122
+ const lpMax = max(values) ?? 1;
19123
+ const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [lpMin, lpMax];
19124
+ const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0, 1];
19125
+ labelPriorityScale = linear2().domain(domain).range(range3);
19126
+ }
19004
19127
  return nodes.map((node) => {
19005
19128
  let radius = DEFAULT_NODE_RADIUS;
19006
19129
  if (sizeScale && encoding.nodeSize?.field) {
@@ -19009,6 +19132,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
19009
19132
  radius = sizeScale(val);
19010
19133
  }
19011
19134
  }
19135
+ let opacity = 1;
19136
+ if (opacityScale && encoding.nodeOpacity?.field) {
19137
+ const val = Number(node[encoding.nodeOpacity.field]);
19138
+ if (Number.isFinite(val)) {
19139
+ opacity = opacityScale(val);
19140
+ }
19141
+ }
19012
19142
  const fill = colorFn ? colorFn(node) : defaultColor;
19013
19143
  const stroke = darkenColor(fill);
19014
19144
  let label;
@@ -19018,8 +19148,14 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
19018
19148
  } else {
19019
19149
  label = node.id;
19020
19150
  }
19021
- const degree = degrees2.get(node.id) ?? 0;
19022
- const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19151
+ let labelPriority;
19152
+ if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
19153
+ const val = Number(node[encoding.nodeLabelPriority.field]);
19154
+ labelPriority = Number.isFinite(val) ? labelPriorityScale(val) : 0;
19155
+ } else {
19156
+ const degree = degrees2.get(node.id) ?? 0;
19157
+ labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
19158
+ }
19023
19159
  const { id: _id, ...rest } = node;
19024
19160
  const data = { id: node.id, ...rest };
19025
19161
  const override = nodeOverrides?.[node.id];
@@ -19037,6 +19173,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
19037
19173
  label,
19038
19174
  labelPriority: finalLabelPriority,
19039
19175
  community: void 0,
19176
+ opacity,
19040
19177
  data
19041
19178
  };
19042
19179
  });
@@ -19069,7 +19206,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19069
19206
  return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
19070
19207
  };
19071
19208
  } else {
19072
- const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
19209
+ const domain = resolveCategoricalDomain(
19210
+ edges.map((e) => String(e[field] ?? "")),
19211
+ encoding.edgeColor.sort,
19212
+ scaleConfig?.domain
19213
+ );
19073
19214
  const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
19074
19215
  const ordinalScale = ordinal().domain(domain).range(range3);
19075
19216
  edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
@@ -19080,10 +19221,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
19080
19221
  let styleFn;
19081
19222
  if (encoding.edgeStyle?.field) {
19082
19223
  const field = encoding.edgeStyle.field;
19083
- const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
19224
+ const domain = resolveCategoricalDomain(
19225
+ edges.map((e) => String(e[field] ?? "")),
19226
+ encoding.edgeStyle.sort,
19227
+ encoding.edgeStyle.scale?.domain
19228
+ );
19084
19229
  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]);
19230
+ for (let i = 0; i < domain.length; i++) {
19231
+ styleMap.set(domain[i], EDGE_STYLES[i % EDGE_STYLES.length]);
19087
19232
  }
19088
19233
  styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
19089
19234
  }
@@ -19141,12 +19286,49 @@ function applyCommunityColors(nodes, colorMap) {
19141
19286
  }
19142
19287
  }
19143
19288
 
19289
+ // src/graphs/interaction.ts
19290
+ var DEFAULT_DIM_OPACITY = 0.15;
19291
+ var DEFAULT_CURSOR_RADIUS = 80;
19292
+ var DEFAULT_CURSOR_STRENGTH = 30;
19293
+ function resolveGraphInteraction(cfg) {
19294
+ const hover = cfg?.hover;
19295
+ const cursor = cfg?.cursorRepulsion;
19296
+ let cursorRepulsion = null;
19297
+ if (cursor === true) {
19298
+ cursorRepulsion = { radius: DEFAULT_CURSOR_RADIUS, strength: DEFAULT_CURSOR_STRENGTH };
19299
+ } else if (cursor && typeof cursor === "object") {
19300
+ cursorRepulsion = {
19301
+ radius: cursor.radius ?? DEFAULT_CURSOR_RADIUS,
19302
+ strength: cursor.strength ?? DEFAULT_CURSOR_STRENGTH
19303
+ };
19304
+ }
19305
+ return {
19306
+ hoverMode: hover?.mode ?? "neighbors",
19307
+ dimOpacity: hover?.dimOpacity ?? DEFAULT_DIM_OPACITY,
19308
+ selectFlyTo: cfg?.select?.flyTo ?? false,
19309
+ cursorRepulsion,
19310
+ springyDrag: cfg?.springyDrag ?? false
19311
+ };
19312
+ }
19313
+
19144
19314
  // src/graphs/compile-graph.ts
19145
19315
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
19146
19316
  var SWATCH_SIZE2 = 12;
19147
19317
  var SWATCH_GAP2 = 6;
19148
19318
  var ENTRY_GAP2 = 16;
19149
- function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
19319
+ var ENERGY_PRESETS = {
19320
+ gentle: { chargeStrength: -150, velocityDecay: 0.5 },
19321
+ balanced: { chargeStrength: -300, velocityDecay: 0.4 },
19322
+ energetic: { chargeStrength: -600, velocityDecay: 0.3 }
19323
+ };
19324
+ var SETTLE_PRESETS = {
19325
+ quick: 0.05,
19326
+ balanced: 0.0228,
19327
+ thorough: 0.01
19328
+ };
19329
+ var DEFAULT_WARMUP_TICKS = 100;
19330
+ var DEFAULT_WARMUP_BUDGET_MS = 250;
19331
+ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField, legendOrder) {
19150
19332
  const labelStyle = {
19151
19333
  fontFamily: theme.fonts.family,
19152
19334
  fontSize: theme.fonts.sizes.small,
@@ -19156,25 +19338,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19156
19338
  };
19157
19339
  let entries;
19158
19340
  if (hasCommunities && communityColorMap.size > 0) {
19341
+ const counts = /* @__PURE__ */ new Map();
19342
+ for (const node of nodes) {
19343
+ if (node.community != null) {
19344
+ counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
19345
+ }
19346
+ }
19159
19347
  entries = [...communityColorMap.entries()].map(([label, color2]) => ({
19160
19348
  label,
19161
19349
  color: color2,
19162
19350
  shape: "circle",
19163
- active: true
19351
+ active: true,
19352
+ count: counts.get(label) ?? 0
19164
19353
  }));
19165
19354
  } else if (nodeColorField) {
19166
19355
  const categoryColors = /* @__PURE__ */ new Map();
19356
+ const categoryCounts = /* @__PURE__ */ new Map();
19167
19357
  for (const node of nodes) {
19168
19358
  const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
19169
19359
  if (!categoryColors.has(category)) {
19170
19360
  categoryColors.set(category, node.fill);
19171
19361
  }
19362
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19172
19363
  }
19173
- entries = categoryColors.size <= 1 ? [] : [...categoryColors.entries()].map(([label, color2]) => ({
19364
+ entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
19174
19365
  label,
19175
- color: color2,
19366
+ color: categoryColors.get(label),
19176
19367
  shape: "circle",
19177
- active: true
19368
+ active: true,
19369
+ count: categoryCounts.get(label) ?? 0
19178
19370
  }));
19179
19371
  } else {
19180
19372
  entries = [];
@@ -19190,6 +19382,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
19190
19382
  swatchChipFill: theme.colors.annotationFill
19191
19383
  };
19192
19384
  }
19385
+ function buildEdgeLegend(edges, encoding) {
19386
+ const field = encoding.edgeColor?.field;
19387
+ if (!field || (encoding.edgeColor?.type ?? "nominal") === "quantitative") return void 0;
19388
+ const categoryColors = /* @__PURE__ */ new Map();
19389
+ const categoryCounts = /* @__PURE__ */ new Map();
19390
+ for (const edge of edges) {
19391
+ const category = String(edge.data[field] ?? "");
19392
+ if (!categoryColors.has(category)) categoryColors.set(category, edge.stroke);
19393
+ categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
19394
+ }
19395
+ if (categoryColors.size <= 1) return void 0;
19396
+ const order = resolveCategoricalDomain(
19397
+ edges.map((e) => String(e.data[field] ?? "")),
19398
+ encoding.edgeColor?.sort,
19399
+ encoding.edgeColor?.scale?.domain
19400
+ );
19401
+ return order.filter((label) => categoryColors.has(label)).map((label) => ({
19402
+ label,
19403
+ color: categoryColors.get(label),
19404
+ shape: "line",
19405
+ active: true,
19406
+ count: categoryCounts.get(label) ?? 0
19407
+ }));
19408
+ }
19193
19409
  function buildGraphTooltips(nodes) {
19194
19410
  const descriptors = /* @__PURE__ */ new Map();
19195
19411
  for (const node of nodes) {
@@ -19216,6 +19432,21 @@ function buildGraphTooltips(nodes) {
19216
19432
  }
19217
19433
  return descriptors;
19218
19434
  }
19435
+ function buildEdgeTooltip(edge) {
19436
+ const fields = [];
19437
+ for (const [key, value2] of Object.entries(edge.data)) {
19438
+ if (key === "source" || key === "target") continue;
19439
+ if (value2 == null) continue;
19440
+ fields.push({
19441
+ label: key,
19442
+ value: typeof value2 === "number" ? graphNumberFormatter(value2) : String(value2)
19443
+ });
19444
+ }
19445
+ return {
19446
+ title: `${edge.source} \u2192 ${edge.target}`,
19447
+ fields
19448
+ };
19449
+ }
19219
19450
  function compileGraph(spec, options) {
19220
19451
  const { spec: normalized, warnings } = compile(spec);
19221
19452
  emitSpecWarnings(warnings, options.onWarn);
@@ -19225,6 +19456,16 @@ function compileGraph(spec, options) {
19225
19456
  );
19226
19457
  }
19227
19458
  const graphSpec = normalized;
19459
+ for (const [name, ch] of [
19460
+ ["nodeColor", graphSpec.encoding.nodeColor],
19461
+ ["edgeColor", graphSpec.encoding.edgeColor]
19462
+ ]) {
19463
+ if (ch?.sort != null && ch.type === "quantitative") {
19464
+ options.onWarn?.(
19465
+ `encoding.${name}.sort is ignored for a quantitative field (sort only orders categorical domains).`
19466
+ );
19467
+ }
19468
+ }
19228
19469
  const rawWatermark = spec.watermark;
19229
19470
  const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
19230
19471
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
@@ -19250,13 +19491,30 @@ function compileGraph(spec, options) {
19250
19491
  }
19251
19492
  const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
19252
19493
  const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
19494
+ const nodeColorField = graphSpec.encoding.nodeColor?.field;
19495
+ const legendOrder = nodeColorField ? resolveCategoricalDomain(
19496
+ compiledNodes.map((n) => String(n.data[nodeColorField] ?? n.label ?? n.id)),
19497
+ graphSpec.encoding.nodeColor?.sort,
19498
+ graphSpec.encoding.nodeColor?.scale?.domain
19499
+ ) : void 0;
19253
19500
  const legend = buildGraphLegend(
19254
19501
  compiledNodes,
19255
19502
  communityColorMap,
19256
19503
  useCommunitiesForLegend,
19257
19504
  theme,
19258
- graphSpec.encoding.nodeColor?.field
19505
+ nodeColorField,
19506
+ legendOrder
19259
19507
  );
19508
+ const edgeLegend = buildEdgeLegend(compiledEdges, graphSpec.encoding);
19509
+ const legendField = nodeColorField ?? (useCommunitiesForLegend ? clusteringField ?? null : null);
19510
+ let initialHighlight;
19511
+ if (nodeColorField && graphSpec.encoding.nodeColor?.highlight != null) {
19512
+ const raw = graphSpec.encoding.nodeColor.highlight;
19513
+ const requested = Array.isArray(raw) ? raw : [raw];
19514
+ const available = new Set(legendOrder ?? []);
19515
+ const values = requested.filter((v) => available.has(v));
19516
+ if (values.length > 0) initialHighlight = { field: nodeColorField, values };
19517
+ }
19260
19518
  const tooltipDescriptors = buildGraphTooltips(compiledNodes);
19261
19519
  const communityCount = communityColorMap.size;
19262
19520
  const altParts = [
@@ -19273,17 +19531,26 @@ function compileGraph(spec, options) {
19273
19531
  };
19274
19532
  const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
19275
19533
  const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
19534
+ const energyPreset = graphSpec.layout.energy ? ENERGY_PRESETS[graphSpec.layout.energy] : ENERGY_PRESETS.balanced;
19535
+ const settlePreset = graphSpec.layout.settle ? SETTLE_PRESETS[graphSpec.layout.settle] : SETTLE_PRESETS.balanced;
19536
+ const warmupRaw = graphSpec.layout.warmup;
19537
+ const warmupTicks = warmupRaw === false ? 0 : typeof warmupRaw === "number" ? Math.max(0, Math.floor(warmupRaw)) : DEFAULT_WARMUP_TICKS;
19276
19538
  const simulationConfig = {
19277
- chargeStrength: graphSpec.layout.chargeStrength ?? -300,
19539
+ chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
19278
19540
  linkDistance: graphSpec.layout.linkDistance ?? 30,
19279
19541
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
19280
- alphaDecay: 0.0228,
19281
- velocityDecay: 0.4,
19542
+ alphaDecay: settlePreset,
19543
+ velocityDecay: energyPreset.velocityDecay,
19282
19544
  collisionRadius: maxRadius + collisionPadding,
19283
19545
  collisionPadding,
19284
19546
  linkStrength: graphSpec.layout.linkStrength,
19285
- centerForce: graphSpec.layout.centerForce
19547
+ centerForce: graphSpec.layout.centerForce,
19548
+ seed: graphSpec.layout.seed,
19549
+ warmupTicks,
19550
+ warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
19286
19551
  };
19552
+ const animation = resolveGraphAnimation(graphSpec.animation);
19553
+ const interaction = resolveGraphInteraction(graphSpec.interaction);
19287
19554
  const chrome = computeChrome4(
19288
19555
  {
19289
19556
  title: graphSpec.chrome.title,
@@ -19312,7 +19579,12 @@ function compileGraph(spec, options) {
19312
19579
  height: options.height
19313
19580
  },
19314
19581
  simulationConfig,
19315
- watermark
19582
+ watermark,
19583
+ animation,
19584
+ interaction,
19585
+ legendField,
19586
+ initialHighlight,
19587
+ edgeLegend
19316
19588
  };
19317
19589
  }
19318
19590
  var DEFAULT_COLLISION_PADDING = 5;
@@ -23739,6 +24011,7 @@ export {
23739
24011
  EXIT_DEFAULTS,
23740
24012
  UPDATE_DEFAULTS,
23741
24013
  buildAreaPath,
24014
+ buildEdgeTooltip,
23742
24015
  buildLinePath,
23743
24016
  clampStaggerDelay,
23744
24017
  clearRenderers,