@opendata-ai/openchart-engine 6.18.0 → 6.19.1

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.18.0",
3
+ "version": "6.19.1",
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.18.0",
48
+ "@opendata-ai/openchart-core": "6.19.1",
49
49
  "d3-array": "^3.2.0",
50
50
  "d3-format": "^3.1.2",
51
51
  "d3-interpolate": "^3.0.0",
@@ -866,8 +866,8 @@ describe('computeAnnotations', () => {
866
866
 
867
867
  // Curve should start from right edge of text, not top edge
868
868
  // Right edge x ≈ label.x + textWidth
869
- // "Curve test" = 10 chars * 12 * 0.55 = 66
870
- expect(connector.from.x).toBeCloseTo(label.x + 66, 1);
869
+ // "Curve test" = 10 chars * 12 * 0.57 = 68.4
870
+ expect(connector.from.x).toBeCloseTo(label.x + 68.4, 1);
871
871
  });
872
872
  });
873
873
 
package/src/compile.ts CHANGED
@@ -240,7 +240,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
240
240
  // Responsive strategy
241
241
  const breakpoint = getBreakpoint(options.width);
242
242
  const heightClass = getHeightClass(options.height);
243
- const strategy = getLayoutStrategy(breakpoint, heightClass);
243
+ let strategy = getLayoutStrategy(breakpoint, heightClass);
244
244
 
245
245
  // Apply breakpoint-conditional overrides from the expanded spec
246
246
  const rawSpec = expandedSpec as Record<string, unknown>;
@@ -286,6 +286,9 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
286
286
  ...chartSpec,
287
287
  annotations: bp.annotations as NormalizedChartSpec['annotations'],
288
288
  };
289
+ // User explicitly provided annotations at this breakpoint — override the
290
+ // responsive strategy so they render inline instead of being stripped.
291
+ strategy = { ...strategy, annotationPosition: 'inline' };
289
292
  }
290
293
  }
291
294
 
@@ -289,9 +289,11 @@ export function computeLegend(
289
289
  }
290
290
 
291
291
  // Top/bottom-positioned legend: horizontal flow with overflow protection.
292
- // Reserve space on the right so legend entries don't overlap the brand watermark.
292
+ // Reserve space on the right for bottom legends so they don't overlap the brand
293
+ // watermark. Top legends don't need this since the brand renders at the bottom.
294
+ const reserveBrand = watermark && resolvedPosition === 'bottom';
293
295
  const availableWidth =
294
- chartArea.width - LEGEND_PADDING * 2 - (watermark ? BRAND_RESERVE_WIDTH : 0);
296
+ chartArea.width - LEGEND_PADDING * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH : 0);
295
297
 
296
298
  // Apply symbolLimit first if set (minimum 1), then fit remaining entries to available rows.
297
299
  if (spec.legend?.symbolLimit != null) {
@@ -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
 
@@ -394,7 +412,15 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
394
412
  height: (node.y1 ?? 0) - (node.y0 ?? 0),
395
413
  fill,
396
414
  cornerRadius: NODE_CORNER_RADIUS,
397
- 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
+ ),
398
424
  nodeId: node.id,
399
425
  value: node.value ?? 0,
400
426
  depth,