@opendata-ai/openchart-engine 6.8.0 → 6.9.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.8.0",
3
+ "version": "6.9.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.8.0",
48
+ "@opendata-ai/openchart-core": "6.9.0",
49
49
  "d3-array": "^3.2.0",
50
50
  "d3-format": "^3.1.2",
51
51
  "d3-interpolate": "^3.0.0",
@@ -246,6 +246,7 @@ function normalizeSankeySpec(spec: SankeySpec, _warnings: string[]): NormalizedS
246
246
  nodeAlign: spec.nodeAlign ?? 'justify',
247
247
  iterations: spec.iterations ?? 6,
248
248
  linkStyle: spec.linkStyle ?? 'gradient',
249
+ nodeLabelAlign: spec.nodeLabelAlign ?? 'auto',
249
250
  chrome: normalizeChrome(spec.chrome),
250
251
  legend: spec.legend,
251
252
  theme: spec.theme ?? {},
@@ -131,18 +131,28 @@ function getLinkColors(
131
131
 
132
132
  /**
133
133
  * Determine label position for a node based on its column depth.
134
- * Leftmost column: label to the right.
135
- * Rightmost column: label to the left.
136
- * Middle columns: label to the right (default).
134
+ * Default ('auto'): leftmost/middle columns label right, rightmost column labels left.
135
+ * 'right': all labels to the right. 'left': all labels to the left.
137
136
  */
138
137
  function computeNodeLabel(
139
138
  node: ComputedNode,
140
139
  maxDepth: number,
141
140
  theme: ResolvedTheme,
142
141
  nodeWidth: number,
142
+ nodeLabelAlign: 'auto' | 'left' | 'right' = 'auto',
143
143
  ): SankeyNodeMark['label'] {
144
144
  const depth = node.depth ?? 0;
145
- const isRightmost = depth === maxDepth;
145
+
146
+ // Determine which side to place the label
147
+ let placeLeft: boolean;
148
+ if (nodeLabelAlign === 'left') {
149
+ placeLeft = true;
150
+ } else if (nodeLabelAlign === 'right') {
151
+ placeLeft = false;
152
+ } else {
153
+ // 'auto': rightmost column goes left, everything else goes right
154
+ placeLeft = depth === maxDepth;
155
+ }
146
156
 
147
157
  const style: TextStyle = {
148
158
  fontFamily: theme.fonts.family,
@@ -158,8 +168,7 @@ function computeNodeLabel(
158
168
  const y1 = node.y1 ?? 0;
159
169
  const midY = (y0 + y1) / 2;
160
170
 
161
- if (isRightmost) {
162
- // Label to the left of the node
171
+ if (placeLeft) {
163
172
  return {
164
173
  text: node.label ?? node.id,
165
174
  x: x0 - LABEL_GAP,
@@ -169,7 +178,6 @@ function computeNodeLabel(
169
178
  };
170
179
  }
171
180
 
172
- // Label to the right of the node (leftmost and middle columns)
173
181
  return {
174
182
  text: node.label ?? node.id,
175
183
  x: x1 + LABEL_GAP,
@@ -311,15 +319,17 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
311
319
  sankeySpec.iterations,
312
320
  );
313
321
 
314
- // 6b. Check if any non-rightmost node labels overflow the right edge.
315
- // Non-rightmost nodes get labels to the right (textAnchor: start),
316
- // which can extend past the drawing area boundary.
322
+ // 6b. Check if any right-side node labels overflow the right edge.
323
+ const nodeLabelAlign = sankeySpec.nodeLabelAlign ?? 'auto';
317
324
  const maxDepthFirst = nodes.reduce((max, n) => Math.max(max, n.depth ?? 0), 0);
318
325
  const rightEdge = area.x + area.width;
319
326
  let maxOverflow = 0;
320
327
  for (const node of nodes) {
321
328
  const depth = node.depth ?? 0;
322
- if (depth === maxDepthFirst) continue; // rightmost labels go left, no overflow
329
+ // Skip nodes whose labels go left (they can't overflow the right edge)
330
+ const labelsLeft =
331
+ nodeLabelAlign === 'left' || (nodeLabelAlign === 'auto' && depth === maxDepthFirst);
332
+ if (labelsLeft) continue;
323
333
  const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
324
334
  const labelText = node.label ?? node.id;
325
335
  const labelWidth = estimateTextWidth(labelText, labelFontSize, labelFontWeight);
@@ -375,7 +385,7 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
375
385
  height: (node.y1 ?? 0) - (node.y0 ?? 0),
376
386
  fill,
377
387
  cornerRadius: NODE_CORNER_RADIUS,
378
- label: computeNodeLabel(node, maxDepth, theme, sankeySpec.nodeWidth),
388
+ label: computeNodeLabel(node, maxDepth, theme, sankeySpec.nodeWidth, nodeLabelAlign),
379
389
  nodeId: node.id,
380
390
  value: node.value ?? 0,
381
391
  depth,
@@ -28,6 +28,7 @@ export interface NormalizedSankeySpec {
28
28
  nodeAlign: SankeyNodeAlign;
29
29
  iterations: number;
30
30
  linkStyle: SankeyLinkColor;
31
+ nodeLabelAlign: 'auto' | 'left' | 'right';
31
32
  chrome: NormalizedChrome;
32
33
  legend?: LegendConfig;
33
34
  theme: ThemeConfig;