@opendata-ai/openchart-engine 6.1.0 → 6.1.2
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 +35 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/layout/dimensions.ts +45 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
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": "
|
|
48
|
+
"@opendata-ai/openchart-core": "6.1.2",
|
|
49
49
|
"d3-array": "^3.2.0",
|
|
50
50
|
"d3-format": "^3.1.2",
|
|
51
51
|
"d3-interpolate": "^3.0.0",
|
package/src/layout/dimensions.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ResolvedTheme,
|
|
23
23
|
} from '@opendata-ai/openchart-core';
|
|
24
24
|
import { computeChrome, estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
25
|
+
import { format as d3Format } from 'd3-format';
|
|
25
26
|
|
|
26
27
|
import type { NormalizedChartSpec, NormalizedChrome } from '../compiler/types';
|
|
27
28
|
|
|
@@ -187,6 +188,27 @@ export function computeDimensions(
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// Reserve right margin for text annotations near the chart's right edge.
|
|
192
|
+
// Without this, annotation text at the last data point clips outside the SVG.
|
|
193
|
+
if (spec.annotations.length > 0 && encoding.x) {
|
|
194
|
+
const xField = encoding.x.field;
|
|
195
|
+
// Find the maximum x value in the data
|
|
196
|
+
let maxX: string | number | undefined;
|
|
197
|
+
for (const row of spec.data) {
|
|
198
|
+
const v = row[xField];
|
|
199
|
+
if (v != null && (maxX == null || String(v) >= String(maxX))) maxX = v as string | number;
|
|
200
|
+
}
|
|
201
|
+
if (maxX != null) {
|
|
202
|
+
const maxXStr = String(maxX);
|
|
203
|
+
for (const ann of spec.annotations) {
|
|
204
|
+
if (ann.type === 'text' && String(ann.x) === maxXStr) {
|
|
205
|
+
const textWidth = estimateTextWidth(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
|
|
206
|
+
margins.right = Math.max(margins.right, padding + textWidth + 12);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
190
212
|
// Dynamic left margin for y-axis labels
|
|
191
213
|
if (encoding.y && !isRadial) {
|
|
192
214
|
if (
|
|
@@ -209,19 +231,33 @@ export function computeDimensions(
|
|
|
209
231
|
} else if (encoding.y.type === 'quantitative' || encoding.y.type === 'temporal') {
|
|
210
232
|
// Numeric tick labels on the left. Estimate width from the data range.
|
|
211
233
|
const yField = encoding.y.field;
|
|
234
|
+
const yAxisFormat = (encoding.y.axis as Record<string, unknown> | undefined)?.format as
|
|
235
|
+
| string
|
|
236
|
+
| undefined;
|
|
212
237
|
let maxAbsVal = 0;
|
|
213
238
|
for (const row of spec.data) {
|
|
214
239
|
const v = Number(row[yField]);
|
|
215
240
|
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
216
241
|
}
|
|
217
|
-
|
|
242
|
+
|
|
218
243
|
let sampleLabel: string;
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
if (yAxisFormat) {
|
|
245
|
+
// Use the actual d3-format to produce a realistic label estimate
|
|
246
|
+
try {
|
|
247
|
+
const fmt = d3Format(yAxisFormat);
|
|
248
|
+
sampleLabel = fmt(maxAbsVal);
|
|
249
|
+
} catch {
|
|
250
|
+
sampleLabel = String(maxAbsVal);
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Fallback: estimate from magnitude
|
|
254
|
+
if (maxAbsVal >= 1_000_000_000) sampleLabel = '1.5B';
|
|
255
|
+
else if (maxAbsVal >= 1_000_000) sampleLabel = '1.5M';
|
|
256
|
+
else if (maxAbsVal >= 1_000) sampleLabel = '1.5K';
|
|
257
|
+
else if (maxAbsVal >= 100) sampleLabel = '100';
|
|
258
|
+
else if (maxAbsVal >= 10) sampleLabel = '10';
|
|
259
|
+
else sampleLabel = '0.0';
|
|
260
|
+
}
|
|
225
261
|
// Account for negative sign
|
|
226
262
|
const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? '-' : '';
|
|
227
263
|
const labelEst = negPrefix + sampleLabel;
|
|
@@ -236,7 +272,8 @@ export function computeDimensions(
|
|
|
236
272
|
}
|
|
237
273
|
|
|
238
274
|
// Rotated y-axis label needs extra left margin (rendered at area.x - 45 in SVG)
|
|
239
|
-
|
|
275
|
+
const yAxis = encoding.y?.axis as Record<string, unknown> | undefined;
|
|
276
|
+
if (yAxis && (yAxis.title || yAxis.label) && !isRadial) {
|
|
240
277
|
const rotatedLabelMargin = 45 + Math.ceil(theme.fonts.sizes.body / 2) + 4;
|
|
241
278
|
margins.left = Math.max(margins.left, padding + rotatedLabelMargin);
|
|
242
279
|
}
|