@internetstiftelsen/charts 0.20.1 → 0.21.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/dist/base-chart.d.ts +1 -1
- package/dist/tooltip/dom.d.ts +5 -1
- package/dist/tooltip/dom.js +37 -9
- package/dist/tooltip/xy-interaction.js +10 -10
- package/dist/types.d.ts +9 -0
- package/dist/x-axis.d.ts +4 -2
- package/dist/x-axis.js +89 -27
- package/dist/xy-chart.d.ts +0 -1
- package/dist/xy-chart.js +18 -10
- package/dist/y-axis.d.ts +4 -1
- package/dist/y-axis.js +44 -12
- package/docs/components.md +54 -9
- package/package.json +1 -1
package/dist/base-chart.d.ts
CHANGED
|
@@ -208,7 +208,7 @@ export declare abstract class BaseChart {
|
|
|
208
208
|
protected prepareForLegendChange(): void;
|
|
209
209
|
protected initializeDataState(): void;
|
|
210
210
|
protected prepareLayout(context: BaseLayoutContext): void;
|
|
211
|
-
|
|
211
|
+
protected applyPlotAreaOverride(plotArea: PlotAreaBounds): PlotAreaBounds;
|
|
212
212
|
/**
|
|
213
213
|
* Setup ResizeObserver for automatic resize handling
|
|
214
214
|
*/
|
package/dist/tooltip/dom.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ type ResolvedTooltipStyle = ChartTheme['tooltip'] & {
|
|
|
11
11
|
};
|
|
12
12
|
export type TooltipStyleOverrides = Partial<Pick<ResolvedTooltipStyle, 'background' | 'border' | 'color' | 'connectorColor'>>;
|
|
13
13
|
export declare class TooltipDom {
|
|
14
|
+
private static activeTooltip;
|
|
14
15
|
private readonly id;
|
|
15
16
|
private readonly splitTooltipOwner;
|
|
16
17
|
private readonly maxWidth;
|
|
@@ -20,6 +21,7 @@ export declare class TooltipDom {
|
|
|
20
21
|
private readonly tooltipPositionResetTimeoutIds;
|
|
21
22
|
private readonly tooltipStyles;
|
|
22
23
|
private tooltipDiv;
|
|
24
|
+
private hideListener;
|
|
23
25
|
constructor(config: TooltipDomConfig);
|
|
24
26
|
initialize(theme: ChartTheme): void;
|
|
25
27
|
getRootTooltip(): TooltipDivSelection | null;
|
|
@@ -28,8 +30,10 @@ export declare class TooltipDom {
|
|
|
28
30
|
getBounds(): DOMRect | null;
|
|
29
31
|
showAt(left: number, top: number): void;
|
|
30
32
|
hasVisibleTooltips(): boolean;
|
|
33
|
+
activate(): void;
|
|
34
|
+
setHideListener(listener: (() => void) | null): void;
|
|
31
35
|
moveVisibleTooltips(deltaX: number, deltaY: number): void;
|
|
32
|
-
hide(): void;
|
|
36
|
+
hide(immediate?: boolean): void;
|
|
33
37
|
cleanup(): void;
|
|
34
38
|
measureTooltip(tooltip: TooltipDivSelection, content: string): {
|
|
35
39
|
width: number;
|
package/dist/tooltip/dom.js
CHANGED
|
@@ -58,6 +58,12 @@ export class TooltipDom {
|
|
|
58
58
|
writable: true,
|
|
59
59
|
value: null
|
|
60
60
|
});
|
|
61
|
+
Object.defineProperty(this, "hideListener", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: null
|
|
66
|
+
});
|
|
61
67
|
this.id = config.id;
|
|
62
68
|
this.splitTooltipOwner = config.splitTooltipOwner;
|
|
63
69
|
this.maxWidth = config.maxWidth;
|
|
@@ -111,6 +117,16 @@ export class TooltipDom {
|
|
|
111
117
|
hasVisibleTooltips() {
|
|
112
118
|
return this.getTooltipNodes().some((node) => this.isTooltipVisible(node));
|
|
113
119
|
}
|
|
120
|
+
activate() {
|
|
121
|
+
if (TooltipDom.activeTooltip === this) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
TooltipDom.activeTooltip?.hide(true);
|
|
125
|
+
TooltipDom.activeTooltip = this;
|
|
126
|
+
}
|
|
127
|
+
setHideListener(listener) {
|
|
128
|
+
this.hideListener = listener;
|
|
129
|
+
}
|
|
114
130
|
moveVisibleTooltips(deltaX, deltaY) {
|
|
115
131
|
this.getTooltipNodes().forEach((node) => {
|
|
116
132
|
if (!this.isTooltipVisible(node)) {
|
|
@@ -124,14 +140,19 @@ export class TooltipDom {
|
|
|
124
140
|
node.style.top = `${position.top + deltaY}px`;
|
|
125
141
|
});
|
|
126
142
|
}
|
|
127
|
-
hide() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.
|
|
131
|
-
}
|
|
132
|
-
|
|
143
|
+
hide(immediate = false) {
|
|
144
|
+
this.hideListener?.();
|
|
145
|
+
this.getTooltipNodes().forEach((node) => {
|
|
146
|
+
this.hideTooltipElement(node, immediate);
|
|
147
|
+
});
|
|
148
|
+
// Retain ownership during fade-out so the next chart can hide it immediately.
|
|
133
149
|
}
|
|
134
150
|
cleanup() {
|
|
151
|
+
this.hide(true);
|
|
152
|
+
this.hideListener = null;
|
|
153
|
+
if (TooltipDom.activeTooltip === this) {
|
|
154
|
+
TooltipDom.activeTooltip = null;
|
|
155
|
+
}
|
|
135
156
|
this.removeRootTooltip();
|
|
136
157
|
this.removeSplitTooltips();
|
|
137
158
|
this.tooltipDiv = null;
|
|
@@ -311,6 +332,7 @@ export class TooltipDom {
|
|
|
311
332
|
if (!node) {
|
|
312
333
|
return;
|
|
313
334
|
}
|
|
335
|
+
this.activate();
|
|
314
336
|
this.cancelTooltipPositionReset(node);
|
|
315
337
|
tooltip.style('visibility', 'visible');
|
|
316
338
|
if (!this.transition.show) {
|
|
@@ -324,7 +346,7 @@ export class TooltipDom {
|
|
|
324
346
|
}
|
|
325
347
|
this.slideTooltipFromOffset(node, slideOffset);
|
|
326
348
|
}
|
|
327
|
-
hideTooltipElement(node) {
|
|
349
|
+
hideTooltipElement(node, immediate = false) {
|
|
328
350
|
const wasVisible = this.isTooltipVisible(node);
|
|
329
351
|
this.cancelTooltipPositionReset(node);
|
|
330
352
|
this.cancelTooltipTransitionFrame(node);
|
|
@@ -333,10 +355,10 @@ export class TooltipDom {
|
|
|
333
355
|
this.resetTooltipPosition(node);
|
|
334
356
|
return;
|
|
335
357
|
}
|
|
336
|
-
node.style.visibility = 'visible';
|
|
358
|
+
node.style.visibility = wasVisible && !immediate ? 'visible' : 'hidden';
|
|
337
359
|
node.style.opacity = '0';
|
|
338
360
|
node.style.transform = TOOLTIP_HIDDEN_TRANSFORM;
|
|
339
|
-
if (!wasVisible) {
|
|
361
|
+
if (!wasVisible || immediate) {
|
|
340
362
|
this.resetTooltipPosition(node);
|
|
341
363
|
return;
|
|
342
364
|
}
|
|
@@ -517,3 +539,9 @@ export class TooltipDom {
|
|
|
517
539
|
}
|
|
518
540
|
}
|
|
519
541
|
}
|
|
542
|
+
Object.defineProperty(TooltipDom, "activeTooltip", {
|
|
543
|
+
enumerable: true,
|
|
544
|
+
configurable: true,
|
|
545
|
+
writable: true,
|
|
546
|
+
value: null
|
|
547
|
+
});
|
|
@@ -378,16 +378,11 @@ export function attachXYTooltipArea(config) {
|
|
|
378
378
|
updateTooltipPosition();
|
|
379
379
|
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
380
380
|
};
|
|
381
|
-
const hideTooltip = () => {
|
|
382
|
-
stopPositionTracking();
|
|
383
|
-
dom.hideTooltipSelection(tooltip);
|
|
384
|
-
dom.hideSplitTooltips();
|
|
385
|
-
clearVisualState();
|
|
386
|
-
};
|
|
387
381
|
const renderTooltip = (request) => {
|
|
388
382
|
if (!tooltipSvgNode) {
|
|
389
383
|
return;
|
|
390
384
|
}
|
|
385
|
+
dom.activate();
|
|
391
386
|
activeSvgBounds = getElementTooltipBounds(tooltipSvgNode);
|
|
392
387
|
if (positionTrackingFrame === null) {
|
|
393
388
|
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
@@ -403,8 +398,14 @@ export function attachXYTooltipArea(config) {
|
|
|
403
398
|
showSharedTooltipAtIndex(request.index);
|
|
404
399
|
};
|
|
405
400
|
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, renderTooltip);
|
|
401
|
+
dom.setHideListener(() => {
|
|
402
|
+
queuedRender.cancel();
|
|
403
|
+
stopPositionTracking();
|
|
404
|
+
clearVisualState();
|
|
405
|
+
});
|
|
406
406
|
overlay
|
|
407
407
|
.on('mousemove', (event) => {
|
|
408
|
+
dom.activate();
|
|
408
409
|
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
409
410
|
const closestIndex = getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal);
|
|
410
411
|
const pointerPosition = getDocumentPointerPosition(event);
|
|
@@ -418,8 +419,7 @@ export function attachXYTooltipArea(config) {
|
|
|
418
419
|
if (isTooltipFocusTarget(document.activeElement)) {
|
|
419
420
|
return;
|
|
420
421
|
}
|
|
421
|
-
|
|
422
|
-
hideTooltip();
|
|
422
|
+
dom.hide();
|
|
423
423
|
});
|
|
424
424
|
const focusTargets = svg
|
|
425
425
|
.append('g')
|
|
@@ -455,8 +455,7 @@ export function attachXYTooltipArea(config) {
|
|
|
455
455
|
if (isTooltipFocusTarget(event.relatedTarget)) {
|
|
456
456
|
return;
|
|
457
457
|
}
|
|
458
|
-
|
|
459
|
-
hideTooltip();
|
|
458
|
+
dom.hide();
|
|
460
459
|
})
|
|
461
460
|
.on('keydown', function (event) {
|
|
462
461
|
const currentIndex = focusTargetNodes.indexOf(this);
|
|
@@ -475,6 +474,7 @@ export function attachXYTooltipArea(config) {
|
|
|
475
474
|
detachScrollListeners?.();
|
|
476
475
|
queuedRender.cancel();
|
|
477
476
|
stopPositionTracking();
|
|
477
|
+
dom.setHideListener(null);
|
|
478
478
|
};
|
|
479
479
|
}
|
|
480
480
|
function normalizeFormatterValue(value) {
|
package/dist/types.d.ts
CHANGED
|
@@ -274,10 +274,15 @@ export type XAxisConfigBase = {
|
|
|
274
274
|
groupLabelMaxWidth?: number;
|
|
275
275
|
groupLabelOversizedBehavior?: LabelOversizedBehavior;
|
|
276
276
|
rotatedLabels?: boolean;
|
|
277
|
+
/** Overrides automatic width for unrotated category labels; also caps numeric/time labels. */
|
|
277
278
|
maxLabelWidth?: number;
|
|
279
|
+
/** Automatically size category labels when maxLabelWidth is omitted. Default: true. */
|
|
280
|
+
autoMaxLabelWidth?: boolean;
|
|
281
|
+
/** Handling for labels exceeding their available width. Default: 'truncate'. */
|
|
278
282
|
oversizedBehavior?: LabelOversizedBehavior;
|
|
279
283
|
tickFormat?: string | AxisTickFormatter | null;
|
|
280
284
|
autoHideOverlapping?: boolean;
|
|
285
|
+
/** Gap for automatic category-label sizing and overlap hiding. Default: 8px. */
|
|
281
286
|
minLabelGap?: number;
|
|
282
287
|
preserveEndLabels?: boolean;
|
|
283
288
|
};
|
|
@@ -288,7 +293,11 @@ export type YAxisConfigBase = {
|
|
|
288
293
|
display?: boolean;
|
|
289
294
|
tickFormat?: string | AxisTickFormatter | null;
|
|
290
295
|
rotatedLabels?: boolean;
|
|
296
|
+
/** Overrides the automatic category-axis budget (one third of inner chart width, less tick padding). */
|
|
291
297
|
maxLabelWidth?: number;
|
|
298
|
+
/** Automatically size category labels when maxLabelWidth is omitted. Default: true. */
|
|
299
|
+
autoMaxLabelWidth?: boolean;
|
|
300
|
+
/** Handling for labels exceeding their available width. Default: 'truncate'. */
|
|
292
301
|
oversizedBehavior?: LabelOversizedBehavior;
|
|
293
302
|
};
|
|
294
303
|
export type YAxisConfig = YAxisConfigBase & {
|
package/dist/x-axis.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
15
15
|
private readonly tickPadding;
|
|
16
16
|
private fontSize;
|
|
17
17
|
private readonly maxLabelWidth?;
|
|
18
|
+
private readonly autoMaxLabelWidth;
|
|
18
19
|
private readonly oversizedBehavior;
|
|
19
20
|
private readonly tickFormat;
|
|
20
21
|
private wrapLineCount;
|
|
@@ -34,7 +35,8 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
34
35
|
* Returns the space required by the x-axis
|
|
35
36
|
*/
|
|
36
37
|
getRequiredSpace(): ComponentSpace;
|
|
37
|
-
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement, data?: DataItem[], estimatedXAxisRangeWidth?: number): void;
|
|
38
|
+
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement, data?: DataItem[], estimatedXAxisRangeWidth?: number, categoryScale?: D3Scale): void;
|
|
39
|
+
private getEstimateLabels;
|
|
38
40
|
clearEstimatedSpace(): void;
|
|
39
41
|
private getTickLabelVerticalFootprint;
|
|
40
42
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, x: D3Scale, theme: ChartTheme, yPosition: number, data?: DataItem[]): void;
|
|
@@ -42,6 +44,7 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
42
44
|
private renderGroupLabels;
|
|
43
45
|
private buildGroupRanges;
|
|
44
46
|
private applyLabelConstraints;
|
|
47
|
+
private getAutomaticLabelWidths;
|
|
45
48
|
private applyGroupLabelConstraints;
|
|
46
49
|
private resolveGroupLabelMaxWidth;
|
|
47
50
|
private wrapTextElement;
|
|
@@ -54,7 +57,6 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
54
57
|
private getLabelBlockHeight;
|
|
55
58
|
private setEstimatedDimensions;
|
|
56
59
|
private createAxisGenerator;
|
|
57
|
-
private applyAxisTextConstraints;
|
|
58
60
|
private applyLabelRotation;
|
|
59
61
|
private resolveGroupRangeInput;
|
|
60
62
|
private resolveGroupLabelKey;
|
package/dist/x-axis.js
CHANGED
|
@@ -8,6 +8,7 @@ const DEFAULT_X_AXIS_CONFIG = {
|
|
|
8
8
|
groupLabelGap: 10,
|
|
9
9
|
groupLabelOversizedBehavior: 'truncate',
|
|
10
10
|
rotatedLabels: false,
|
|
11
|
+
autoMaxLabelWidth: true,
|
|
11
12
|
oversizedBehavior: 'truncate',
|
|
12
13
|
tickFormat: null,
|
|
13
14
|
autoHideOverlapping: false,
|
|
@@ -119,6 +120,12 @@ export class XAxis {
|
|
|
119
120
|
writable: true,
|
|
120
121
|
value: void 0
|
|
121
122
|
});
|
|
123
|
+
Object.defineProperty(this, "autoMaxLabelWidth", {
|
|
124
|
+
enumerable: true,
|
|
125
|
+
configurable: true,
|
|
126
|
+
writable: true,
|
|
127
|
+
value: void 0
|
|
128
|
+
});
|
|
122
129
|
Object.defineProperty(this, "oversizedBehavior", {
|
|
123
130
|
enumerable: true,
|
|
124
131
|
configurable: true,
|
|
@@ -194,6 +201,7 @@ export class XAxis {
|
|
|
194
201
|
resolvedConfig.groupLabelOversizedBehavior;
|
|
195
202
|
this.rotatedLabels = resolvedConfig.rotatedLabels;
|
|
196
203
|
this.maxLabelWidth = resolvedConfig.maxLabelWidth;
|
|
204
|
+
this.autoMaxLabelWidth = resolvedConfig.autoMaxLabelWidth;
|
|
197
205
|
this.oversizedBehavior = resolvedConfig.oversizedBehavior;
|
|
198
206
|
this.tickFormat = resolvedConfig.tickFormat;
|
|
199
207
|
this.autoHideOverlapping = resolvedConfig.autoHideOverlapping;
|
|
@@ -213,6 +221,7 @@ export class XAxis {
|
|
|
213
221
|
groupLabelOversizedBehavior: this.groupLabelOversizedBehavior,
|
|
214
222
|
rotatedLabels: this.rotatedLabels,
|
|
215
223
|
maxLabelWidth: this.maxLabelWidth,
|
|
224
|
+
autoMaxLabelWidth: this.autoMaxLabelWidth,
|
|
216
225
|
oversizedBehavior: this.oversizedBehavior,
|
|
217
226
|
tickFormat: this.tickFormat,
|
|
218
227
|
autoHideOverlapping: this.autoHideOverlapping,
|
|
@@ -250,9 +259,7 @@ export class XAxis {
|
|
|
250
259
|
const baseHeight = this.tickPadding + this.fontSize + 5;
|
|
251
260
|
let height = this.rotatedLabels ? baseHeight * 2.5 : baseHeight;
|
|
252
261
|
// Account for wrapped text height (multiply by estimated line count)
|
|
253
|
-
if (this.
|
|
254
|
-
this.oversizedBehavior === 'wrap' &&
|
|
255
|
-
this.wrapLineCount > 1) {
|
|
262
|
+
if (this.oversizedBehavior === 'wrap' && this.wrapLineCount > 1) {
|
|
256
263
|
height += (this.wrapLineCount - 1) * this.fontSize * 1.2;
|
|
257
264
|
}
|
|
258
265
|
if (this.showGroupLabels) {
|
|
@@ -267,7 +274,7 @@ export class XAxis {
|
|
|
267
274
|
position: 'bottom',
|
|
268
275
|
};
|
|
269
276
|
}
|
|
270
|
-
estimateLayoutSpace(labels, theme, svg, data = [], estimatedXAxisRangeWidth) {
|
|
277
|
+
estimateLayoutSpace(labels, theme, svg, data = [], estimatedXAxisRangeWidth, categoryScale) {
|
|
271
278
|
if (!labels.length) {
|
|
272
279
|
this.estimatedHeight = null;
|
|
273
280
|
this.estimatedTickLabelVerticalFootprint = null;
|
|
@@ -281,12 +288,13 @@ export class XAxis {
|
|
|
281
288
|
const fontWeight = theme.axis.fontWeight || 'normal';
|
|
282
289
|
let maxWidth = 0;
|
|
283
290
|
let maxLines = 1;
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
const automaticWidths = this.getAutomaticLabelWidths(categoryScale);
|
|
292
|
+
const tickLabels = this.getEstimateLabels(labels, data, categoryScale);
|
|
293
|
+
for (const { value, text } of tickLabels) {
|
|
286
294
|
if (!text) {
|
|
287
295
|
continue;
|
|
288
296
|
}
|
|
289
|
-
const measurement = this.measureLabel(text, fontSize, fontFamily, fontWeight, svg);
|
|
297
|
+
const measurement = this.measureLabel(text, fontSize, fontFamily, fontWeight, svg, this.maxLabelWidth ?? automaticWidths.get(value));
|
|
290
298
|
maxLines = Math.max(maxLines, measurement.lines);
|
|
291
299
|
maxWidth = Math.max(maxWidth, measurement.width);
|
|
292
300
|
}
|
|
@@ -294,10 +302,22 @@ export class XAxis {
|
|
|
294
302
|
this.setEstimatedDimensions(maxWidth, maxLines, theme);
|
|
295
303
|
this.wrapLineCount = maxLines;
|
|
296
304
|
}
|
|
305
|
+
getEstimateLabels(labels, data, scale) {
|
|
306
|
+
const formatter = scale
|
|
307
|
+
? this.createAxisGenerator(scale, this.buildLabelLookup(data)).tickFormat()
|
|
308
|
+
: null;
|
|
309
|
+
return (scale?.domain() ?? labels).map((value, index) => ({
|
|
310
|
+
value: value,
|
|
311
|
+
text: formatter
|
|
312
|
+
? formatter(value, index)
|
|
313
|
+
: String(value ?? ''),
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
297
316
|
clearEstimatedSpace() {
|
|
298
317
|
this.estimatedHeight = null;
|
|
299
318
|
this.estimatedTickLabelVerticalFootprint = null;
|
|
300
319
|
this.groupLabelWrapLineCount = 1;
|
|
320
|
+
this.wrapLineCount = 1;
|
|
301
321
|
}
|
|
302
322
|
getTickLabelVerticalFootprint() {
|
|
303
323
|
if (this.estimatedTickLabelVerticalFootprint !== null) {
|
|
@@ -325,7 +345,9 @@ export class XAxis {
|
|
|
325
345
|
.attr('font-family', theme.axis.fontFamily)
|
|
326
346
|
.attr('font-weight', theme.axis.fontWeight || 'normal')
|
|
327
347
|
.attr('stroke', 'none');
|
|
328
|
-
this.
|
|
348
|
+
this.fontSize = this.resolveFontSizeValue(theme.axis.fontSize, this.fontSize);
|
|
349
|
+
this.wrapLineCount = 1;
|
|
350
|
+
this.applyLabelConstraints(axis, svg.node(), theme.axis.fontSize, theme.axis.fontFamily, theme.axis.fontWeight || 'normal', x);
|
|
329
351
|
this.applyLabelRotation(axis);
|
|
330
352
|
this.applyAutoHiding(axis, x);
|
|
331
353
|
axis.selectAll('.domain').remove();
|
|
@@ -419,15 +441,21 @@ export class XAxis {
|
|
|
419
441
|
}
|
|
420
442
|
return ranges.filter((range) => range.label.trim() !== '');
|
|
421
443
|
}
|
|
422
|
-
applyLabelConstraints(axisGroup, svg, fontSize, fontFamily, fontWeight) {
|
|
423
|
-
|
|
424
|
-
return;
|
|
425
|
-
const maxWidth = this.maxLabelWidth;
|
|
444
|
+
applyLabelConstraints(axisGroup, svg, fontSize, fontFamily, fontWeight, scale) {
|
|
445
|
+
const automaticWidths = this.getAutomaticLabelWidths(scale);
|
|
426
446
|
const behavior = this.oversizedBehavior;
|
|
427
447
|
axisGroup
|
|
428
448
|
.selectAll('text')
|
|
429
|
-
.each((
|
|
449
|
+
.each((value, i, nodes) => {
|
|
430
450
|
const textEl = nodes[i];
|
|
451
|
+
const maxWidth = this.maxLabelWidth ??
|
|
452
|
+
automaticWidths.get(value);
|
|
453
|
+
if (maxWidth === undefined)
|
|
454
|
+
return;
|
|
455
|
+
if (maxWidth <= 0) {
|
|
456
|
+
textEl.style.visibility = 'hidden';
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
431
459
|
const originalText = textEl.textContent || '';
|
|
432
460
|
const textWidth = measureTextWidth(originalText, fontSize, fontFamily, fontWeight, svg);
|
|
433
461
|
if (textWidth <= maxWidth) {
|
|
@@ -454,6 +482,35 @@ export class XAxis {
|
|
|
454
482
|
}
|
|
455
483
|
});
|
|
456
484
|
}
|
|
485
|
+
getAutomaticLabelWidths(scale) {
|
|
486
|
+
const widths = new Map();
|
|
487
|
+
if (!this.autoMaxLabelWidth ||
|
|
488
|
+
this.maxLabelWidth !== undefined ||
|
|
489
|
+
this.rotatedLabels ||
|
|
490
|
+
!scale?.bandwidth) {
|
|
491
|
+
return widths;
|
|
492
|
+
}
|
|
493
|
+
const domain = scale.domain();
|
|
494
|
+
const halfBand = scale.bandwidth() / 2;
|
|
495
|
+
const positions = domain.map((value) => scale(value) + halfBand);
|
|
496
|
+
const rangeStart = Math.min(...scale.range());
|
|
497
|
+
const rangeEnd = Math.max(...scale.range());
|
|
498
|
+
domain.forEach((value, index) => {
|
|
499
|
+
const position = positions[index];
|
|
500
|
+
// Centered labels share the distance between neighboring ticks.
|
|
501
|
+
// Edge labels must also stay inside the axis range.
|
|
502
|
+
const previousGap = index > 0
|
|
503
|
+
? Math.abs(position - positions[index - 1]) -
|
|
504
|
+
this.minLabelGap
|
|
505
|
+
: Infinity;
|
|
506
|
+
const nextGap = index < domain.length - 1
|
|
507
|
+
? Math.abs(positions[index + 1] - position) -
|
|
508
|
+
this.minLabelGap
|
|
509
|
+
: Infinity;
|
|
510
|
+
widths.set(value, Math.max(0, Math.min(previousGap, nextGap, 2 * (position - rangeStart), 2 * (rangeEnd - position))));
|
|
511
|
+
});
|
|
512
|
+
return widths;
|
|
513
|
+
}
|
|
457
514
|
applyGroupLabelConstraints(groupLabels, svg, groupLabelStyle) {
|
|
458
515
|
groupLabels.each((range, i, nodes) => {
|
|
459
516
|
const textEl = nodes[i];
|
|
@@ -507,7 +564,7 @@ export class XAxis {
|
|
|
507
564
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
508
565
|
tspan.textContent = line;
|
|
509
566
|
tspan.setAttribute('x', textEl.getAttribute('x') || '0');
|
|
510
|
-
tspan.setAttribute('dy', i === 0 ? '0' : `${lineHeight}px`);
|
|
567
|
+
tspan.setAttribute('dy', i === 0 ? textEl.getAttribute('dy') || '0' : `${lineHeight}px`);
|
|
511
568
|
textEl.appendChild(tspan);
|
|
512
569
|
});
|
|
513
570
|
// Add tooltip with full text
|
|
@@ -541,19 +598,27 @@ export class XAxis {
|
|
|
541
598
|
this.hideLastOverlappingIntervalLabel(labelEntries, skipInterval);
|
|
542
599
|
}
|
|
543
600
|
}
|
|
544
|
-
measureLabel(text, fontSize, fontFamily, fontWeight, svg) {
|
|
545
|
-
if (
|
|
546
|
-
|
|
601
|
+
measureLabel(text, fontSize, fontFamily, fontWeight, svg, maxWidth) {
|
|
602
|
+
if (maxWidth !== undefined && maxWidth <= 0) {
|
|
603
|
+
return { width: 0, lines: 1 };
|
|
604
|
+
}
|
|
605
|
+
if (maxWidth !== undefined && this.oversizedBehavior === 'wrap') {
|
|
606
|
+
const lines = wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg);
|
|
547
607
|
return {
|
|
548
|
-
width:
|
|
608
|
+
width: Math.max(...lines.map((line) => measureTextWidth(line, fontSize, fontFamily, fontWeight, svg))),
|
|
549
609
|
lines: lines.length || 1,
|
|
550
610
|
};
|
|
551
611
|
}
|
|
552
612
|
const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
|
|
613
|
+
if (maxWidth !== undefined &&
|
|
614
|
+
this.oversizedBehavior === 'hide' &&
|
|
615
|
+
textWidth > maxWidth) {
|
|
616
|
+
return { width: 0, lines: 1 };
|
|
617
|
+
}
|
|
553
618
|
return {
|
|
554
|
-
width:
|
|
555
|
-
?
|
|
556
|
-
: textWidth,
|
|
619
|
+
width: maxWidth === undefined
|
|
620
|
+
? textWidth
|
|
621
|
+
: Math.min(textWidth, maxWidth),
|
|
557
622
|
lines: 1,
|
|
558
623
|
};
|
|
559
624
|
}
|
|
@@ -664,12 +729,6 @@ export class XAxis {
|
|
|
664
729
|
axisGenerator.ticks(5, tickFormat);
|
|
665
730
|
return axisGenerator;
|
|
666
731
|
}
|
|
667
|
-
applyAxisTextConstraints(axis, svg, theme) {
|
|
668
|
-
if (!this.maxLabelWidth) {
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
this.applyLabelConstraints(axis, svg, theme.axis.fontSize, theme.axis.fontFamily, theme.axis.fontWeight || 'normal');
|
|
672
|
-
}
|
|
673
732
|
applyLabelRotation(axis) {
|
|
674
733
|
if (!this.rotatedLabels) {
|
|
675
734
|
return;
|
|
@@ -742,6 +801,9 @@ export class XAxis {
|
|
|
742
801
|
}
|
|
743
802
|
applyAutoHideVisibility(labelEntries, skipInterval) {
|
|
744
803
|
labelEntries.forEach((textEl, index) => {
|
|
804
|
+
// Overflow hiding takes precedence over interval/end-label rules.
|
|
805
|
+
if (textEl.style.visibility === 'hidden')
|
|
806
|
+
return;
|
|
745
807
|
const isFirst = index === 0;
|
|
746
808
|
const isLast = index === labelEntries.length - 1;
|
|
747
809
|
const isAtInterval = index % skipInterval === 0;
|
package/dist/xy-chart.d.ts
CHANGED
|
@@ -26,7 +26,6 @@ export declare class XYChart extends BaseChart {
|
|
|
26
26
|
update(data: ChartData): void;
|
|
27
27
|
protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
|
|
28
28
|
protected prepareLayout(context: BaseLayoutContext): void;
|
|
29
|
-
private getEstimatedXAxisRangeWidth;
|
|
30
29
|
private getYAxisEstimateLabels;
|
|
31
30
|
private createContinuousScaleForLayoutEstimate;
|
|
32
31
|
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
package/dist/xy-chart.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ChartValidationError, ChartValidator } from './validation.js';
|
|
|
4
4
|
import { GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
|
|
5
5
|
import { resolveScaleValue } from './scale-utils.js';
|
|
6
6
|
import { mergeDeep } from './utils.js';
|
|
7
|
+
import { LayoutManager } from './layout-manager.js';
|
|
7
8
|
import { createXYMotionDriver, } from './xy-motion/driver.js';
|
|
8
9
|
import { createXYSeriesSnapshotId } from './xy-motion/helpers.js';
|
|
9
10
|
const DEFAULT_SERIES_COLOR = '#8884d8';
|
|
@@ -153,10 +154,15 @@ export class XYChart extends BaseChart {
|
|
|
153
154
|
}
|
|
154
155
|
prepareLayout(context) {
|
|
155
156
|
super.prepareLayout(context);
|
|
156
|
-
this.xAxis?.clearEstimatedSpace
|
|
157
|
-
this.yAxis?.clearEstimatedSpace
|
|
157
|
+
this.xAxis?.clearEstimatedSpace();
|
|
158
|
+
this.yAxis?.clearEstimatedSpace();
|
|
159
|
+
const { x: xConfig, y: yConfig } = this.getResolvedAxisConfigs();
|
|
158
160
|
if (this.yAxis) {
|
|
159
|
-
this.yAxis.estimateLayoutSpace
|
|
161
|
+
this.yAxis.estimateLayoutSpace(this.getYAxisEstimateLabels(), this.renderTheme, context.svgNode, yConfig.type === 'band'
|
|
162
|
+
? Math.max(0, this.width -
|
|
163
|
+
this.renderTheme.margins.left -
|
|
164
|
+
this.renderTheme.margins.right) / 3
|
|
165
|
+
: undefined);
|
|
160
166
|
}
|
|
161
167
|
if (this.xAxis) {
|
|
162
168
|
const xKey = this.getXKey();
|
|
@@ -167,15 +173,17 @@ export class XYChart extends BaseChart {
|
|
|
167
173
|
}
|
|
168
174
|
return item[xKey];
|
|
169
175
|
});
|
|
170
|
-
this.
|
|
176
|
+
const plotArea = this.applyPlotAreaOverride(new LayoutManager(this.resolvedRenderTheme).calculateLayout(this.getLayoutComponents()));
|
|
177
|
+
const range = [
|
|
178
|
+
plotArea.left + 10,
|
|
179
|
+
Math.max(plotArea.left + 10, plotArea.right),
|
|
180
|
+
];
|
|
181
|
+
const categoryScale = xConfig.type === 'band'
|
|
182
|
+
? this.buildScale('band', this.resolveScaleDomain(xConfig, xKey), xConfig.reverse ? [range[1], range[0]] : range, xConfig)
|
|
183
|
+
: undefined;
|
|
184
|
+
this.xAxis.estimateLayoutSpace(labels, this.renderTheme, context.svgNode, this.data, range[1] - range[0], categoryScale);
|
|
171
185
|
}
|
|
172
186
|
}
|
|
173
|
-
getEstimatedXAxisRangeWidth() {
|
|
174
|
-
const yAxisWidth = this.yAxis?.getRequiredSpace().width ?? 0;
|
|
175
|
-
const { left, right } = this.renderTheme.margins;
|
|
176
|
-
const xScalePadding = 10;
|
|
177
|
-
return Math.max(0, this.width - left - right - yAxisWidth - xScalePadding);
|
|
178
|
-
}
|
|
179
187
|
getYAxisEstimateLabels() {
|
|
180
188
|
if (!this.yAxis) {
|
|
181
189
|
return [];
|
package/dist/y-axis.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
|
|
|
7
7
|
private readonly tickPadding;
|
|
8
8
|
private fontSize;
|
|
9
9
|
private readonly maxLabelWidth?;
|
|
10
|
+
private readonly autoMaxLabelWidth;
|
|
11
|
+
private automaticMaxLabelWidth?;
|
|
10
12
|
private readonly tickFormat;
|
|
11
13
|
private readonly rotatedLabels;
|
|
12
14
|
private readonly oversizedBehavior;
|
|
@@ -20,8 +22,9 @@ export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
|
|
|
20
22
|
* Returns the space required by the y-axis
|
|
21
23
|
*/
|
|
22
24
|
getRequiredSpace(): ComponentSpace;
|
|
23
|
-
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement): void;
|
|
25
|
+
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement, categoryAxisWidth?: number): void;
|
|
24
26
|
clearEstimatedSpace(): void;
|
|
27
|
+
private getAutomaticLabelWidth;
|
|
25
28
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, y: D3Scale, theme: ChartTheme, xPosition: number): void;
|
|
26
29
|
private applyLabelConstraints;
|
|
27
30
|
private measureLabelDimensions;
|
package/dist/y-axis.js
CHANGED
|
@@ -44,6 +44,18 @@ export class YAxis {
|
|
|
44
44
|
writable: true,
|
|
45
45
|
value: void 0
|
|
46
46
|
});
|
|
47
|
+
Object.defineProperty(this, "autoMaxLabelWidth", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
value: void 0
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(this, "automaticMaxLabelWidth", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: void 0
|
|
58
|
+
});
|
|
47
59
|
Object.defineProperty(this, "tickFormat", {
|
|
48
60
|
enumerable: true,
|
|
49
61
|
configurable: true,
|
|
@@ -74,11 +86,12 @@ export class YAxis {
|
|
|
74
86
|
writable: true,
|
|
75
87
|
value: void 0
|
|
76
88
|
});
|
|
77
|
-
const { display = true, tickFormat = null, rotatedLabels = false, maxLabelWidth, oversizedBehavior = 'truncate', exportHooks, } = config ?? {};
|
|
89
|
+
const { display = true, tickFormat = null, rotatedLabels = false, maxLabelWidth, autoMaxLabelWidth = true, oversizedBehavior = 'truncate', exportHooks, } = config ?? {};
|
|
78
90
|
this.display = display;
|
|
79
91
|
this.tickFormat = tickFormat;
|
|
80
92
|
this.rotatedLabels = rotatedLabels;
|
|
81
93
|
this.maxLabelWidth = maxLabelWidth;
|
|
94
|
+
this.autoMaxLabelWidth = autoMaxLabelWidth;
|
|
82
95
|
this.oversizedBehavior = oversizedBehavior;
|
|
83
96
|
this.exportHooks = exportHooks;
|
|
84
97
|
}
|
|
@@ -88,6 +101,7 @@ export class YAxis {
|
|
|
88
101
|
tickFormat: this.tickFormat,
|
|
89
102
|
rotatedLabels: this.rotatedLabels,
|
|
90
103
|
maxLabelWidth: this.maxLabelWidth,
|
|
104
|
+
autoMaxLabelWidth: this.autoMaxLabelWidth,
|
|
91
105
|
oversizedBehavior: this.oversizedBehavior,
|
|
92
106
|
};
|
|
93
107
|
}
|
|
@@ -124,7 +138,9 @@ export class YAxis {
|
|
|
124
138
|
position: 'left',
|
|
125
139
|
};
|
|
126
140
|
}
|
|
127
|
-
estimateLayoutSpace(labels, theme, svg) {
|
|
141
|
+
estimateLayoutSpace(labels, theme, svg, categoryAxisWidth) {
|
|
142
|
+
this.automaticMaxLabelWidth =
|
|
143
|
+
this.getAutomaticLabelWidth(categoryAxisWidth);
|
|
128
144
|
if (!labels.length) {
|
|
129
145
|
this.estimatedWidth = 0;
|
|
130
146
|
return;
|
|
@@ -139,7 +155,7 @@ export class YAxis {
|
|
|
139
155
|
const text = String(label ?? '');
|
|
140
156
|
if (!text)
|
|
141
157
|
continue;
|
|
142
|
-
const { width, height } = this.measureLabelDimensions(text, fontSize, fontFamily, fontWeight, svg);
|
|
158
|
+
const { width, height } = this.measureLabelDimensions(text, fontSize, fontFamily, fontWeight, svg, this.maxLabelWidth ?? this.automaticMaxLabelWidth);
|
|
143
159
|
maxWidth = Math.max(maxWidth, width);
|
|
144
160
|
maxHeight = Math.max(maxHeight, height);
|
|
145
161
|
}
|
|
@@ -151,6 +167,16 @@ export class YAxis {
|
|
|
151
167
|
}
|
|
152
168
|
clearEstimatedSpace() {
|
|
153
169
|
this.estimatedWidth = null;
|
|
170
|
+
this.automaticMaxLabelWidth = undefined;
|
|
171
|
+
}
|
|
172
|
+
getAutomaticLabelWidth(categoryAxisWidth) {
|
|
173
|
+
if (!this.autoMaxLabelWidth ||
|
|
174
|
+
this.rotatedLabels ||
|
|
175
|
+
categoryAxisWidth === undefined) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
// Vertical tick spacing is a height, so use a chart-width budget instead.
|
|
179
|
+
return Math.max(0, categoryAxisWidth - this.tickPadding);
|
|
154
180
|
}
|
|
155
181
|
render(svg, y, theme, xPosition) {
|
|
156
182
|
if (!this.display) {
|
|
@@ -180,7 +206,8 @@ export class YAxis {
|
|
|
180
206
|
.attr('font-family', theme.axis.fontFamily)
|
|
181
207
|
.attr('font-weight', theme.axis.fontWeight || 'normal');
|
|
182
208
|
// Apply label constraints before rotation
|
|
183
|
-
this.applyLabelConstraints(axisGroup, svg.node(), theme.axis.fontSize, theme.axis.fontFamily, theme.axis.fontWeight || 'normal'
|
|
209
|
+
this.applyLabelConstraints(axisGroup, svg.node(), theme.axis.fontSize, theme.axis.fontFamily, theme.axis.fontWeight || 'normal', this.maxLabelWidth ??
|
|
210
|
+
(y.bandwidth ? this.automaticMaxLabelWidth : undefined));
|
|
184
211
|
// Apply rotation to labels if enabled
|
|
185
212
|
if (this.rotatedLabels) {
|
|
186
213
|
axisGroup
|
|
@@ -190,16 +217,19 @@ export class YAxis {
|
|
|
190
217
|
}
|
|
191
218
|
axisGroup.selectAll('.domain').remove();
|
|
192
219
|
}
|
|
193
|
-
applyLabelConstraints(axisGroup, svg, fontSize, fontFamily, fontWeight) {
|
|
194
|
-
if (
|
|
220
|
+
applyLabelConstraints(axisGroup, svg, fontSize, fontFamily, fontWeight, maxWidth) {
|
|
221
|
+
if (maxWidth === undefined) {
|
|
195
222
|
return;
|
|
196
223
|
}
|
|
197
|
-
const maxWidth = this.maxLabelWidth;
|
|
198
224
|
const behavior = this.oversizedBehavior;
|
|
199
225
|
axisGroup
|
|
200
226
|
.selectAll('text')
|
|
201
227
|
.each((_d, i, nodes) => {
|
|
202
228
|
const textEl = nodes[i];
|
|
229
|
+
if (maxWidth <= 0) {
|
|
230
|
+
textEl.style.visibility = 'hidden';
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
203
233
|
const originalText = textEl.textContent || '';
|
|
204
234
|
const textWidth = measureTextWidth(originalText, fontSize, fontFamily, fontWeight, svg);
|
|
205
235
|
if (textWidth <= maxWidth) {
|
|
@@ -226,11 +256,13 @@ export class YAxis {
|
|
|
226
256
|
}
|
|
227
257
|
});
|
|
228
258
|
}
|
|
229
|
-
measureLabelDimensions(text, fontSize, fontFamily, fontWeight, svg) {
|
|
259
|
+
measureLabelDimensions(text, fontSize, fontFamily, fontWeight, svg, maxWidth) {
|
|
260
|
+
if (maxWidth !== undefined && maxWidth <= 0) {
|
|
261
|
+
return { width: 0, height: 0 };
|
|
262
|
+
}
|
|
230
263
|
const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
|
|
231
264
|
const lineHeight = fontSize * 1.2;
|
|
232
|
-
if (
|
|
233
|
-
textWidth <= this.maxLabelWidth) {
|
|
265
|
+
if (maxWidth === undefined || textWidth <= maxWidth) {
|
|
234
266
|
return {
|
|
235
267
|
width: textWidth,
|
|
236
268
|
height: fontSize,
|
|
@@ -239,11 +271,11 @@ export class YAxis {
|
|
|
239
271
|
switch (this.oversizedBehavior) {
|
|
240
272
|
case 'truncate':
|
|
241
273
|
return {
|
|
242
|
-
width:
|
|
274
|
+
width: maxWidth,
|
|
243
275
|
height: fontSize,
|
|
244
276
|
};
|
|
245
277
|
case 'wrap': {
|
|
246
|
-
const lines = wrapText(text,
|
|
278
|
+
const lines = wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg);
|
|
247
279
|
const widestLine = lines.reduce((widest, line) => {
|
|
248
280
|
return Math.max(widest, measureTextWidth(line, fontSize, fontFamily, fontWeight, svg));
|
|
249
281
|
}, 0);
|
package/docs/components.md
CHANGED
|
@@ -17,15 +17,36 @@ new XAxis({
|
|
|
17
17
|
groupLabelMaxWidth?: number, // Optional cap for grouped-label width
|
|
18
18
|
groupLabelOversizedBehavior?: 'truncate' | 'wrap' | 'hide', // Defaults to truncate
|
|
19
19
|
rotatedLabels?: boolean, // Rotate tick labels -45deg
|
|
20
|
-
maxLabelWidth?: number, //
|
|
21
|
-
|
|
20
|
+
maxLabelWidth?: number, // Override automatic category-label width, in pixels
|
|
21
|
+
autoMaxLabelWidth?: boolean, // Automatically size category labels (default: true)
|
|
22
|
+
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // Default: truncate
|
|
22
23
|
autoHideOverlapping?: boolean, // Automatically hide overlapping labels
|
|
23
|
-
minLabelGap?: number, //
|
|
24
|
-
preserveEndLabels?: boolean, //
|
|
24
|
+
minLabelGap?: number, // Gap for automatic category sizing and auto-hide (default: 8px)
|
|
25
|
+
preserveEndLabels?: boolean, // Preserve end labels during auto-hide unless overflow hides them
|
|
25
26
|
tickFormat?: string | ((value: string | number | Date) => string) | null
|
|
26
27
|
})
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
Unrotated category (band-scale) labels automatically fit the rendered axis when
|
|
31
|
+
`maxLabelWidth` is omitted. Each label uses the distance to its neighboring ticks,
|
|
32
|
+
less `minLabelGap`; the first and last labels are also bounded by the axis range.
|
|
33
|
+
This uses the scale's actual domain and positions, including band padding,
|
|
34
|
+
reversed axes, and grouped gaps.
|
|
35
|
+
|
|
36
|
+
`oversizedBehavior` controls labels that exceed that space: `wrap` creates
|
|
37
|
+
multiple lines (including breaking long words), `truncate` adds an ellipsis,
|
|
38
|
+
and `hide` hides the label. Wrapping and truncation retain the full text in an
|
|
39
|
+
SVG title. Widths and wrapped bottom margins are recalculated on resize and
|
|
40
|
+
chart updates, using the display text from `labelKey` or `tickFormat`.
|
|
41
|
+
|
|
42
|
+
An explicit `maxLabelWidth` overrides automatic sizing. Numeric/time axes and
|
|
43
|
+
rotated labels retain their existing uncapped behavior unless a width is supplied.
|
|
44
|
+
Set `autoMaxLabelWidth: false` to disable automatic sizing and leave category
|
|
45
|
+
labels uncapped too. An explicit `maxLabelWidth` still applies when this is off;
|
|
46
|
+
group-label sizing is controlled separately.
|
|
47
|
+
`autoHideOverlapping` remains a separate interval-based visibility option;
|
|
48
|
+
`preserveEndLabels` does not restore labels hidden by `oversizedBehavior: 'hide'`.
|
|
49
|
+
|
|
29
50
|
Grouped label styles come from `theme.axis.groupLabel` and are bold by
|
|
30
51
|
default. When `groupLabelMaxWidth` is omitted, grouped labels are automatically
|
|
31
52
|
capped to their rendered group range so adjacent group labels do not collide.
|
|
@@ -38,6 +59,14 @@ that cap.
|
|
|
38
59
|
chart.addChild(new XAxis({ dataKey: 'date' }));
|
|
39
60
|
```
|
|
40
61
|
|
|
62
|
+
Automatically wrap category labels such as “ChatGPT”, “Copilot”, and
|
|
63
|
+
“My AI (på Snapchat)” without a fixed pixel width:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
chart.addChild(new XAxis({ dataKey: 'category', oversizedBehavior: 'wrap' }));
|
|
67
|
+
// Use oversizedBehavior: 'truncate' or 'hide' for the other overflow modes.
|
|
68
|
+
```
|
|
69
|
+
|
|
41
70
|
Callback formatter example:
|
|
42
71
|
|
|
43
72
|
```typescript
|
|
@@ -67,15 +96,26 @@ Renders the Y axis.
|
|
|
67
96
|
new YAxis({
|
|
68
97
|
display?: boolean, // Render axis and reserve layout space (default: true)
|
|
69
98
|
rotatedLabels?: boolean, // Rotate tick labels -45deg
|
|
70
|
-
maxLabelWidth?: number, //
|
|
71
|
-
|
|
99
|
+
maxLabelWidth?: number, // Override automatic category-label width, in pixels
|
|
100
|
+
autoMaxLabelWidth?: boolean, // Automatically size category labels (default: true)
|
|
101
|
+
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // Default: truncate
|
|
72
102
|
tickFormat?: string | ((value: string | number | Date) => string) | null
|
|
73
103
|
})
|
|
74
104
|
```
|
|
75
105
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
For unrotated category axes (including horizontal bar charts), omitting
|
|
107
|
+
`maxLabelWidth` automatically budgets up to one third of the chart width inside
|
|
108
|
+
the outer left/right margins for the axis. The label cap is that budget minus
|
|
109
|
+
the 10px tick padding. Vertical tick spacing is a height, so it is not used as a
|
|
110
|
+
text-width limit. Shorter labels reserve only their measured width, and wrapped
|
|
111
|
+
lines are centered vertically on each tick.
|
|
112
|
+
|
|
113
|
+
The same `wrap`, `truncate`, and `hide` modes apply, and sizing is recalculated on
|
|
114
|
+
resize and chart updates. An explicit `maxLabelWidth` overrides the budget.
|
|
115
|
+
Set `autoMaxLabelWidth: false` to disable the automatic budget; an explicit
|
|
116
|
+
`maxLabelWidth` still applies.
|
|
117
|
+
Numeric/time axes and rotated labels continue to reserve their measured width
|
|
118
|
+
unless `maxLabelWidth` is supplied.
|
|
79
119
|
|
|
80
120
|
### Format Examples
|
|
81
121
|
|
|
@@ -123,6 +163,11 @@ chart.addChild(new Grid());
|
|
|
123
163
|
|
|
124
164
|
Renders interactive tooltips on hover and keyboard focus.
|
|
125
165
|
|
|
166
|
+
Only one chart can show tooltips at a time, whether charts are standalone or
|
|
167
|
+
inside a `ChartGroup`. Hovering, tapping, or focusing another chart dismisses
|
|
168
|
+
the previous chart's tooltips immediately, including during transitions.
|
|
169
|
+
Split mode can still show multiple series tooltips within the active chart.
|
|
170
|
+
|
|
126
171
|
```typescript
|
|
127
172
|
new Tooltip({
|
|
128
173
|
mode?: 'shared' | 'split' | 'single',
|
package/package.json
CHANGED