@opendata-ai/openchart-engine 6.24.2 → 6.25.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/dist/index.js +5904 -5577
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/compile-layer.test.ts +321 -0
- package/src/charts/line/area.ts +9 -4
- package/src/compile.ts +471 -10
- package/src/layout/axes/ticks.ts +4 -3
- package/src/layout/axes.ts +6 -4
- package/src/layout/dimensions.ts +57 -15
package/src/layout/dimensions.ts
CHANGED
|
@@ -21,7 +21,23 @@ import type {
|
|
|
21
21
|
ResolvedChrome,
|
|
22
22
|
ResolvedTheme,
|
|
23
23
|
} from '@opendata-ai/openchart-core';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
AXIS_TITLE_TRAILING_PAD,
|
|
26
|
+
BREAKPOINT_COMPACT_MAX,
|
|
27
|
+
computeChrome,
|
|
28
|
+
estimateTextWidth,
|
|
29
|
+
getAxisTitleOffset,
|
|
30
|
+
HPAD_COMPACT_FRACTION,
|
|
31
|
+
HPAD_COMPACT_MIN,
|
|
32
|
+
LABEL_GAP_COMPACT,
|
|
33
|
+
LABEL_GAP_DEFAULT,
|
|
34
|
+
MAX_LEFT_LABEL_FRACTION_COMPACT,
|
|
35
|
+
MAX_LEFT_LABEL_FRACTION_DEFAULT,
|
|
36
|
+
MAX_LEFT_LABEL_FRACTION_MEDIUM,
|
|
37
|
+
MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
|
|
38
|
+
NARROW_VIEWPORT_MAX,
|
|
39
|
+
TOP_PAD_EXTRA_NARROW,
|
|
40
|
+
} from '@opendata-ai/openchart-core';
|
|
25
41
|
import { format as d3Format } from 'd3-format';
|
|
26
42
|
|
|
27
43
|
import type { NormalizedChartSpec, NormalizedChrome } from '../compiler/types';
|
|
@@ -102,6 +118,12 @@ export function computeDimensions(
|
|
|
102
118
|
const { width, height } = options;
|
|
103
119
|
|
|
104
120
|
const padding = scalePadding(theme.spacing.padding, width, height);
|
|
121
|
+
// Horizontal padding can be tighter than the chrome text padding on narrow
|
|
122
|
+
// containers because axis titles and tick labels tolerate closer edges.
|
|
123
|
+
const hPad =
|
|
124
|
+
width < BREAKPOINT_COMPACT_MAX
|
|
125
|
+
? Math.max(Math.round(padding * HPAD_COMPACT_FRACTION), HPAD_COMPACT_MIN)
|
|
126
|
+
: padding;
|
|
105
127
|
const axisMargin = theme.spacing.axisMargin;
|
|
106
128
|
const chromeMode = strategy?.chromeMode ?? 'full';
|
|
107
129
|
|
|
@@ -160,11 +182,14 @@ export function computeDimensions(
|
|
|
160
182
|
// added when there's actual chrome content that needs separation from the
|
|
161
183
|
// chart area. When chrome is empty the margin is just padding.
|
|
162
184
|
const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin;
|
|
185
|
+
// Extra top padding on narrow viewports prevents iOS Safari from clipping
|
|
186
|
+
// the title chrome behind the browser UI.
|
|
187
|
+
const topPad = width < NARROW_VIEWPORT_MAX ? padding + TOP_PAD_EXTRA_NARROW : padding;
|
|
163
188
|
const margins: Margins = {
|
|
164
|
-
top:
|
|
165
|
-
right:
|
|
189
|
+
top: topPad + chrome.topHeight + topAxisGap,
|
|
190
|
+
right: hPad + (isRadial ? hPad : axisMargin),
|
|
166
191
|
bottom: padding + chrome.bottomHeight + xAxisHeight,
|
|
167
|
-
left:
|
|
192
|
+
left: hPad + (isRadial ? hPad : axisMargin),
|
|
168
193
|
};
|
|
169
194
|
|
|
170
195
|
// Dynamic right margin for line/area end-of-line labels.
|
|
@@ -191,7 +216,7 @@ export function computeDimensions(
|
|
|
191
216
|
}
|
|
192
217
|
}
|
|
193
218
|
if (maxLabelWidth > 0) {
|
|
194
|
-
margins.right = Math.max(margins.right,
|
|
219
|
+
margins.right = Math.max(margins.right, hPad + maxLabelWidth + 8);
|
|
195
220
|
}
|
|
196
221
|
}
|
|
197
222
|
}
|
|
@@ -232,7 +257,7 @@ export function computeDimensions(
|
|
|
232
257
|
textWidth / 2; // centered (top/bottom/auto)
|
|
233
258
|
const rightOverflow = Math.max(0, baseRightExtent + dx);
|
|
234
259
|
if (rightOverflow > 0) {
|
|
235
|
-
margins.right = Math.max(margins.right,
|
|
260
|
+
margins.right = Math.max(margins.right, hPad + rightOverflow + 12);
|
|
236
261
|
}
|
|
237
262
|
}
|
|
238
263
|
}
|
|
@@ -258,13 +283,18 @@ export function computeDimensions(
|
|
|
258
283
|
}
|
|
259
284
|
if (maxLabelWidth > 0) {
|
|
260
285
|
// Tighter label-to-chart gap on narrow containers
|
|
261
|
-
const labelGap = width <
|
|
286
|
+
const labelGap = width < NARROW_VIEWPORT_MAX ? LABEL_GAP_COMPACT : LABEL_GAP_DEFAULT;
|
|
262
287
|
// Clamp reservation so bars keep at least ~45% of container width on
|
|
263
288
|
// narrow viewports. Labels that exceed the cap will be truncated by
|
|
264
289
|
// the axis renderer (see axes.ts).
|
|
265
|
-
const maxLeftFraction =
|
|
290
|
+
const maxLeftFraction =
|
|
291
|
+
width < BREAKPOINT_COMPACT_MAX
|
|
292
|
+
? MAX_LEFT_LABEL_FRACTION_COMPACT
|
|
293
|
+
: width < MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX
|
|
294
|
+
? MAX_LEFT_LABEL_FRACTION_MEDIUM
|
|
295
|
+
: MAX_LEFT_LABEL_FRACTION_DEFAULT;
|
|
266
296
|
const maxLeftReserved = Math.floor(width * maxLeftFraction);
|
|
267
|
-
const reserved = Math.min(
|
|
297
|
+
const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
|
|
268
298
|
margins.left = Math.max(margins.left, reserved);
|
|
269
299
|
}
|
|
270
300
|
} else if (encoding.y.type === 'quantitative' || encoding.y.type === 'temporal') {
|
|
@@ -306,15 +336,26 @@ export function computeDimensions(
|
|
|
306
336
|
theme.fonts.weights.normal,
|
|
307
337
|
);
|
|
308
338
|
// 6px gap between label and chart area edge
|
|
309
|
-
margins.left = Math.max(margins.left,
|
|
339
|
+
margins.left = Math.max(margins.left, hPad + labelWidth + 10);
|
|
310
340
|
}
|
|
311
341
|
}
|
|
312
342
|
|
|
313
|
-
// Rotated y-axis label needs extra left margin (rendered at area.x -
|
|
343
|
+
// Rotated y-axis label needs extra left margin (rendered at area.x - offset in SVG).
|
|
344
|
+
// Tighter on compact viewports where horizontal space is scarce.
|
|
314
345
|
const yAxis = encoding.y?.axis as Record<string, unknown> | undefined;
|
|
315
346
|
if (yAxis && (yAxis.title || yAxis.label) && !isRadial) {
|
|
316
|
-
const
|
|
317
|
-
|
|
347
|
+
const axisTitleOffset = getAxisTitleOffset(width);
|
|
348
|
+
const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
|
|
349
|
+
const rotatedLabelMargin =
|
|
350
|
+
axisTitleOffset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX ? 0 : AXIS_TITLE_TRAILING_PAD);
|
|
351
|
+
margins.left = Math.max(margins.left, hPad + rotatedLabelMargin);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Reserve space for a secondary (right) y-axis in dual-axis charts.
|
|
355
|
+
// Use Math.max (not +=) to mirror the left-margin pattern: the reserve
|
|
356
|
+
// replaces the base axisMargin when it's larger, instead of stacking.
|
|
357
|
+
if (options.rightAxisReserve && options.rightAxisReserve > 0) {
|
|
358
|
+
margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
|
|
318
359
|
}
|
|
319
360
|
|
|
320
361
|
// Reserve legend space
|
|
@@ -354,9 +395,10 @@ export function computeDimensions(
|
|
|
354
395
|
watermark,
|
|
355
396
|
);
|
|
356
397
|
|
|
357
|
-
// Recalculate top/bottom margins with stripped chrome
|
|
398
|
+
// Recalculate top/bottom margins with stripped chrome.
|
|
399
|
+
// Use topPad (not padding) to preserve the iOS Safari clearance on narrow viewports.
|
|
358
400
|
const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin;
|
|
359
|
-
const newTop =
|
|
401
|
+
const newTop = topPad + fallbackChrome.topHeight + fallbackTopAxisGap;
|
|
360
402
|
const topDelta = margins.top - newTop;
|
|
361
403
|
const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
|
|
362
404
|
const bottomDelta = margins.bottom - newBottom;
|