@opendata-ai/openchart-engine 6.17.0 → 6.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-engine",
3
- "version": "6.17.0",
3
+ "version": "6.19.0",
4
4
  "description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@opendata-ai/openchart-core": "6.17.0",
48
+ "@opendata-ai/openchart-core": "6.19.0",
49
49
  "d3-array": "^3.2.0",
50
50
  "d3-format": "^3.1.2",
51
51
  "d3-interpolate": "^3.0.0",
@@ -249,6 +249,7 @@ function normalizeSankeySpec(spec: SankeySpec, _warnings: string[]): NormalizedS
249
249
  iterations: spec.iterations ?? 6,
250
250
  linkStyle: spec.linkStyle ?? 'gradient',
251
251
  nodeLabelAlign: spec.nodeLabelAlign ?? 'auto',
252
+ nodeSort: spec.nodeSort,
252
253
  chrome: normalizeChrome(spec.chrome),
253
254
  legend: spec.legend,
254
255
  theme: spec.theme ?? {},
@@ -140,6 +140,8 @@ function computeNodeLabel(
140
140
  theme: ResolvedTheme,
141
141
  nodeWidth: number,
142
142
  nodeLabelAlign: 'auto' | 'left' | 'right' = 'auto',
143
+ containerWidth?: number,
144
+ padding?: number,
143
145
  ): SankeyNodeMark['label'] {
144
146
  const depth = node.depth ?? 0;
145
147
 
@@ -168,6 +170,20 @@ function computeNodeLabel(
168
170
  const y1 = node.y1 ?? 0;
169
171
  const midY = (y0 + y1) / 2;
170
172
 
173
+ // Compute maxWidth: space from label position to the container edge
174
+ const pad = padding ?? 0;
175
+ let maxWidth: number | undefined;
176
+ if (containerWidth !== undefined) {
177
+ if (placeLeft) {
178
+ // Label goes left from x0: available space is from left padding to x0
179
+ maxWidth = x0 - LABEL_GAP - pad;
180
+ } else {
181
+ // Label goes right from x1: available space is from x1 to right edge
182
+ maxWidth = containerWidth - pad - (x1 + LABEL_GAP);
183
+ }
184
+ if (maxWidth !== undefined && maxWidth < 0) maxWidth = 0;
185
+ }
186
+
171
187
  if (placeLeft) {
172
188
  return {
173
189
  text: node.label ?? node.id,
@@ -175,6 +191,7 @@ function computeNodeLabel(
175
191
  y: midY,
176
192
  style: { ...style, textAnchor: 'end', dominantBaseline: 'central' },
177
193
  visible: true,
194
+ maxWidth,
178
195
  };
179
196
  }
180
197
 
@@ -184,6 +201,7 @@ function computeNodeLabel(
184
201
  y: midY,
185
202
  style: { ...style, textAnchor: 'start', dominantBaseline: 'central' },
186
203
  visible: true,
204
+ maxWidth,
187
205
  };
188
206
  }
189
207
 
@@ -324,6 +342,7 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
324
342
  sankeySpec.nodePadding,
325
343
  sankeySpec.nodeAlign,
326
344
  sankeySpec.iterations,
345
+ sankeySpec.nodeSort,
327
346
  );
328
347
 
329
348
  // 6b. Check if any right-side node labels overflow the right edge.
@@ -363,6 +382,7 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
363
382
  sankeySpec.nodePadding,
364
383
  sankeySpec.nodeAlign,
365
384
  sankeySpec.iterations,
385
+ sankeySpec.nodeSort,
366
386
  ));
367
387
  }
368
388
 
@@ -392,7 +412,15 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
392
412
  height: (node.y1 ?? 0) - (node.y0 ?? 0),
393
413
  fill,
394
414
  cornerRadius: NODE_CORNER_RADIUS,
395
- label: computeNodeLabel(node, maxDepth, theme, sankeySpec.nodeWidth, nodeLabelAlign),
415
+ label: computeNodeLabel(
416
+ node,
417
+ maxDepth,
418
+ theme,
419
+ sankeySpec.nodeWidth,
420
+ nodeLabelAlign,
421
+ options.width,
422
+ padding,
423
+ ),
396
424
  nodeId: node.id,
397
425
  value: node.value ?? 0,
398
426
  depth,
@@ -73,6 +73,7 @@ export function computeSankeyLayout(
73
73
  nodePadding: number,
74
74
  nodeAlign: SankeyNodeAlign,
75
75
  iterations: number,
76
+ nodeSort?: string[],
76
77
  ): SankeyLayoutResult {
77
78
  // Extract unique node IDs from source and target columns
78
79
  const nodeSet = new Set<string>();
@@ -113,6 +114,19 @@ export function computeSankeyLayout(
113
114
  ])
114
115
  .iterations(iterations);
115
116
 
117
+ // Apply explicit node ordering when provided.
118
+ // Builds a comparator from the ordered ID array so d3-sankey places nodes
119
+ // top-to-bottom within each column according to the spec's nodeSort.
120
+ if (nodeSort && nodeSort.length > 0) {
121
+ const orderMap = new Map(nodeSort.map((id, i) => [id, i]));
122
+ const fallback = nodeSort.length;
123
+ generator.nodeSort(
124
+ (a: SankeyNode<NodeExtra, LinkExtra>, b: SankeyNode<NodeExtra, LinkExtra>) =>
125
+ (orderMap.get((a as unknown as NodeExtra).id) ?? fallback) -
126
+ (orderMap.get((b as unknown as NodeExtra).id) ?? fallback),
127
+ );
128
+ }
129
+
116
130
  const graph = generator({
117
131
  nodes: nodes as unknown as Array<SankeyNode<NodeExtra, LinkExtra>>,
118
132
  links: links as unknown as Array<SankeyLink<NodeExtra, LinkExtra>>,
@@ -29,6 +29,7 @@ export interface NormalizedSankeySpec {
29
29
  iterations: number;
30
30
  linkStyle: SankeyLinkColor;
31
31
  nodeLabelAlign: 'auto' | 'left' | 'right';
32
+ nodeSort?: string[];
32
33
  chrome: NormalizedChrome;
33
34
  legend?: LegendConfig;
34
35
  theme: ThemeConfig;