@internetstiftelsen/charts 0.20.0 → 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 +8 -1
- package/dist/tooltip/dom.js +55 -8
- package/dist/tooltip/xy-interaction.js +65 -43
- 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 +59 -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;
|
|
@@ -27,7 +29,11 @@ export declare class TooltipDom {
|
|
|
27
29
|
setContent(content: string): void;
|
|
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;
|
|
35
|
+
moveVisibleTooltips(deltaX: number, deltaY: number): void;
|
|
36
|
+
hide(immediate?: boolean): void;
|
|
31
37
|
cleanup(): void;
|
|
32
38
|
measureTooltip(tooltip: TooltipDivSelection, content: string): {
|
|
33
39
|
width: number;
|
|
@@ -50,6 +56,7 @@ export declare class TooltipDom {
|
|
|
50
56
|
private scheduleTooltipPositionReset;
|
|
51
57
|
private cancelTooltipPositionReset;
|
|
52
58
|
private getTooltipTransitionStyle;
|
|
59
|
+
private getTooltipNodes;
|
|
53
60
|
private isTooltipVisible;
|
|
54
61
|
private getTooltipPosition;
|
|
55
62
|
private hasVisibleSlideOffset;
|
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;
|
|
@@ -108,14 +114,45 @@ export class TooltipDom {
|
|
|
108
114
|
}
|
|
109
115
|
this.showTooltipAt(this.tooltipDiv, left, top);
|
|
110
116
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
hasVisibleTooltips() {
|
|
118
|
+
return this.getTooltipNodes().some((node) => this.isTooltipVisible(node));
|
|
119
|
+
}
|
|
120
|
+
activate() {
|
|
121
|
+
if (TooltipDom.activeTooltip === this) {
|
|
122
|
+
return;
|
|
115
123
|
}
|
|
116
|
-
|
|
124
|
+
TooltipDom.activeTooltip?.hide(true);
|
|
125
|
+
TooltipDom.activeTooltip = this;
|
|
126
|
+
}
|
|
127
|
+
setHideListener(listener) {
|
|
128
|
+
this.hideListener = listener;
|
|
129
|
+
}
|
|
130
|
+
moveVisibleTooltips(deltaX, deltaY) {
|
|
131
|
+
this.getTooltipNodes().forEach((node) => {
|
|
132
|
+
if (!this.isTooltipVisible(node)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const position = this.getTooltipPosition(node);
|
|
136
|
+
if (!position) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
node.style.left = `${position.left + deltaX}px`;
|
|
140
|
+
node.style.top = `${position.top + deltaY}px`;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
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.
|
|
117
149
|
}
|
|
118
150
|
cleanup() {
|
|
151
|
+
this.hide(true);
|
|
152
|
+
this.hideListener = null;
|
|
153
|
+
if (TooltipDom.activeTooltip === this) {
|
|
154
|
+
TooltipDom.activeTooltip = null;
|
|
155
|
+
}
|
|
119
156
|
this.removeRootTooltip();
|
|
120
157
|
this.removeSplitTooltips();
|
|
121
158
|
this.tooltipDiv = null;
|
|
@@ -295,6 +332,7 @@ export class TooltipDom {
|
|
|
295
332
|
if (!node) {
|
|
296
333
|
return;
|
|
297
334
|
}
|
|
335
|
+
this.activate();
|
|
298
336
|
this.cancelTooltipPositionReset(node);
|
|
299
337
|
tooltip.style('visibility', 'visible');
|
|
300
338
|
if (!this.transition.show) {
|
|
@@ -308,7 +346,7 @@ export class TooltipDom {
|
|
|
308
346
|
}
|
|
309
347
|
this.slideTooltipFromOffset(node, slideOffset);
|
|
310
348
|
}
|
|
311
|
-
hideTooltipElement(node) {
|
|
349
|
+
hideTooltipElement(node, immediate = false) {
|
|
312
350
|
const wasVisible = this.isTooltipVisible(node);
|
|
313
351
|
this.cancelTooltipPositionReset(node);
|
|
314
352
|
this.cancelTooltipTransitionFrame(node);
|
|
@@ -317,10 +355,10 @@ export class TooltipDom {
|
|
|
317
355
|
this.resetTooltipPosition(node);
|
|
318
356
|
return;
|
|
319
357
|
}
|
|
320
|
-
node.style.visibility = 'visible';
|
|
358
|
+
node.style.visibility = wasVisible && !immediate ? 'visible' : 'hidden';
|
|
321
359
|
node.style.opacity = '0';
|
|
322
360
|
node.style.transform = TOOLTIP_HIDDEN_TRANSFORM;
|
|
323
|
-
if (!wasVisible) {
|
|
361
|
+
if (!wasVisible || immediate) {
|
|
324
362
|
this.resetTooltipPosition(node);
|
|
325
363
|
return;
|
|
326
364
|
}
|
|
@@ -350,6 +388,9 @@ export class TooltipDom {
|
|
|
350
388
|
getTooltipTransitionStyle() {
|
|
351
389
|
return `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`;
|
|
352
390
|
}
|
|
391
|
+
getTooltipNodes() {
|
|
392
|
+
return Array.from(document.querySelectorAll(`#${this.id}, [data-chart-tooltip-owner="${this.splitTooltipOwner}"]`));
|
|
393
|
+
}
|
|
353
394
|
isTooltipVisible(node) {
|
|
354
395
|
return (node.style.visibility === 'visible' && node.style.opacity !== '0');
|
|
355
396
|
}
|
|
@@ -498,3 +539,9 @@ export class TooltipDom {
|
|
|
498
539
|
}
|
|
499
540
|
}
|
|
500
541
|
}
|
|
542
|
+
Object.defineProperty(TooltipDom, "activeTooltip", {
|
|
543
|
+
enumerable: true,
|
|
544
|
+
configurable: true,
|
|
545
|
+
writable: true,
|
|
546
|
+
value: null
|
|
547
|
+
});
|
|
@@ -301,11 +301,11 @@ export function attachXYTooltipArea(config) {
|
|
|
301
301
|
const svgNode = svg.node();
|
|
302
302
|
if (!svgNode) {
|
|
303
303
|
dom.hideTooltipSelection(tooltip);
|
|
304
|
-
return
|
|
304
|
+
return;
|
|
305
305
|
}
|
|
306
306
|
const placementBounds = getTooltipPlacementBounds(svgNode);
|
|
307
307
|
const candidates = [];
|
|
308
|
-
series.forEach((currentSeries
|
|
308
|
+
series.forEach((currentSeries) => {
|
|
309
309
|
const rawValue = dataPoint[currentSeries.dataKey];
|
|
310
310
|
if (rawValue === null || rawValue === undefined) {
|
|
311
311
|
return;
|
|
@@ -319,59 +319,93 @@ export function attachXYTooltipArea(config) {
|
|
|
319
319
|
const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
|
|
320
320
|
candidates.push({
|
|
321
321
|
series: currentSeries,
|
|
322
|
-
seriesIndex,
|
|
323
322
|
anchor: visibleAnchor,
|
|
324
323
|
target,
|
|
325
324
|
});
|
|
326
325
|
});
|
|
327
|
-
const selectedCandidate =
|
|
326
|
+
const selectedCandidate = getClosestSingleTooltipCandidate(candidates, request);
|
|
328
327
|
updateVisualStateAtIndex(request.index);
|
|
329
328
|
dom.hideSplitTooltips();
|
|
330
329
|
if (!selectedCandidate) {
|
|
331
330
|
dom.hideTooltipSelection(tooltip);
|
|
332
|
-
return
|
|
331
|
+
return;
|
|
333
332
|
}
|
|
334
333
|
dom.applyRootTooltipStyles(theme, getSeriesTooltipStyle(selectedCandidate.series, dataPoint, request.index));
|
|
335
334
|
const content = buildSplitTooltipContent(dataPoint, selectedCandidate.series);
|
|
336
335
|
const measuredTooltip = dom.measureTooltip(tooltip, content);
|
|
337
336
|
if (!measuredTooltip) {
|
|
338
337
|
dom.hideTooltipSelection(tooltip);
|
|
339
|
-
return
|
|
338
|
+
return;
|
|
340
339
|
}
|
|
341
340
|
const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, placementBounds);
|
|
342
341
|
const tooltipPosition = getAnchoredTooltipPosition(selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
|
|
343
342
|
if (!tooltipPosition) {
|
|
344
343
|
dom.hideTooltipSelection(tooltip);
|
|
345
|
-
return
|
|
344
|
+
return;
|
|
346
345
|
}
|
|
347
346
|
dom.renderTooltipWithConnector(tooltip, arrowEdge, tooltipPosition.left, tooltipPosition.top, measuredTooltip.width, measuredTooltip.height, selectedCandidate.target.x, selectedCandidate.target.y, selectedCandidate.anchor);
|
|
348
|
-
return {
|
|
349
|
-
index: request.index,
|
|
350
|
-
seriesIndex: selectedCandidate.seriesIndex,
|
|
351
|
-
};
|
|
352
|
-
};
|
|
353
|
-
let activeTooltipRequest = null;
|
|
354
|
-
const hideTooltip = () => {
|
|
355
|
-
activeTooltipRequest = null;
|
|
356
|
-
dom.hideTooltipSelection(tooltip);
|
|
357
|
-
dom.hideSplitTooltips();
|
|
358
|
-
clearVisualState();
|
|
359
347
|
};
|
|
360
348
|
const tooltipSvgNode = svg.node();
|
|
361
|
-
|
|
349
|
+
let activeSvgBounds = null;
|
|
350
|
+
let positionTrackingFrame = null;
|
|
351
|
+
const stopPositionTracking = () => {
|
|
352
|
+
activeSvgBounds = null;
|
|
353
|
+
if (positionTrackingFrame !== null) {
|
|
354
|
+
cancelFrame(positionTrackingFrame);
|
|
355
|
+
positionTrackingFrame = null;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const updateTooltipPosition = () => {
|
|
359
|
+
if (!activeSvgBounds || !tooltipSvgNode) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const nextSvgBounds = getElementTooltipBounds(tooltipSvgNode);
|
|
363
|
+
const deltaX = nextSvgBounds.minLeft - activeSvgBounds.minLeft;
|
|
364
|
+
const deltaY = nextSvgBounds.minTop - activeSvgBounds.minTop;
|
|
365
|
+
if (deltaX === 0 && deltaY === 0) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
dom.moveVisibleTooltips(deltaX, deltaY);
|
|
369
|
+
activeSvgBounds = nextSvgBounds;
|
|
370
|
+
};
|
|
371
|
+
const trackTooltipPosition = () => {
|
|
372
|
+
positionTrackingFrame = null;
|
|
373
|
+
if (!tooltipSvgNode?.isConnected || !dom.hasVisibleTooltips()) {
|
|
374
|
+
stopPositionTracking();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Scroll events can arrive late during touch or momentum scrolling.
|
|
378
|
+
updateTooltipPosition();
|
|
379
|
+
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
380
|
+
};
|
|
381
|
+
const renderTooltip = (request) => {
|
|
382
|
+
if (!tooltipSvgNode) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
dom.activate();
|
|
386
|
+
activeSvgBounds = getElementTooltipBounds(tooltipSvgNode);
|
|
387
|
+
if (positionTrackingFrame === null) {
|
|
388
|
+
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
389
|
+
}
|
|
362
390
|
if (mode === 'single') {
|
|
363
|
-
|
|
391
|
+
showSingleTooltip(request);
|
|
364
392
|
return;
|
|
365
393
|
}
|
|
366
|
-
activeTooltipRequest = { index: request.index };
|
|
367
394
|
if (mode === 'split') {
|
|
368
395
|
showSplitTooltipAtIndex(request.index);
|
|
369
396
|
return;
|
|
370
397
|
}
|
|
371
398
|
showSharedTooltipAtIndex(request.index);
|
|
399
|
+
};
|
|
400
|
+
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, renderTooltip);
|
|
401
|
+
dom.setHideListener(() => {
|
|
402
|
+
queuedRender.cancel();
|
|
403
|
+
stopPositionTracking();
|
|
404
|
+
clearVisualState();
|
|
372
405
|
});
|
|
373
406
|
overlay
|
|
374
407
|
.on('mousemove', (event) => {
|
|
408
|
+
dom.activate();
|
|
375
409
|
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
376
410
|
const closestIndex = getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal);
|
|
377
411
|
const pointerPosition = getDocumentPointerPosition(event);
|
|
@@ -385,8 +419,7 @@ export function attachXYTooltipArea(config) {
|
|
|
385
419
|
if (isTooltipFocusTarget(document.activeElement)) {
|
|
386
420
|
return;
|
|
387
421
|
}
|
|
388
|
-
|
|
389
|
-
hideTooltip();
|
|
422
|
+
dom.hide();
|
|
390
423
|
});
|
|
391
424
|
const focusTargets = svg
|
|
392
425
|
.append('g')
|
|
@@ -415,26 +448,14 @@ export function attachXYTooltipArea(config) {
|
|
|
415
448
|
}
|
|
416
449
|
queuedRender.cancel();
|
|
417
450
|
select(this).attr('stroke', '#111827').attr('stroke-width', 2);
|
|
418
|
-
|
|
419
|
-
activeTooltipRequest = showSingleTooltip({
|
|
420
|
-
index: currentIndex,
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
activeTooltipRequest = { index: currentIndex };
|
|
425
|
-
if (mode === 'split') {
|
|
426
|
-
showSplitTooltipAtIndex(currentIndex);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
showSharedTooltipAtIndex(currentIndex);
|
|
451
|
+
renderTooltip({ index: currentIndex });
|
|
430
452
|
})
|
|
431
453
|
.on('blur', function (event) {
|
|
432
454
|
select(this).attr('stroke', 'none').attr('stroke-width', 0);
|
|
433
455
|
if (isTooltipFocusTarget(event.relatedTarget)) {
|
|
434
456
|
return;
|
|
435
457
|
}
|
|
436
|
-
|
|
437
|
-
hideTooltip();
|
|
458
|
+
dom.hide();
|
|
438
459
|
})
|
|
439
460
|
.on('keydown', function (event) {
|
|
440
461
|
const currentIndex = focusTargetNodes.indexOf(this);
|
|
@@ -448,12 +469,13 @@ export function attachXYTooltipArea(config) {
|
|
|
448
469
|
event.preventDefault();
|
|
449
470
|
focusTargetNodes[nextIndex].focus();
|
|
450
471
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
472
|
+
const detachScrollListeners = attachTooltipScrollListeners(tooltipSvgNode, updateTooltipPosition);
|
|
473
|
+
return () => {
|
|
474
|
+
detachScrollListeners?.();
|
|
475
|
+
queuedRender.cancel();
|
|
476
|
+
stopPositionTracking();
|
|
477
|
+
dom.setHideListener(null);
|
|
478
|
+
};
|
|
457
479
|
}
|
|
458
480
|
function normalizeFormatterValue(value) {
|
|
459
481
|
if (value === null ||
|
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',
|
|
@@ -167,6 +212,11 @@ paths are omitted when the arrow is already close to its target. For line,
|
|
|
167
212
|
area, and scatter points, side split tooltips spread nearby point targets
|
|
168
213
|
within the connectorless arrow range before drawing connector paths.
|
|
169
214
|
|
|
215
|
+
Visible XY tooltips track their targets on every animation frame, including
|
|
216
|
+
during touch and momentum scrolling. Their initial placement is preserved,
|
|
217
|
+
allowing them to pass beyond the container edges instead of sticking there
|
|
218
|
+
until blur. Tracking stops when the tooltips are hidden or the chart is destroyed.
|
|
219
|
+
|
|
170
220
|
For horizontal bar charts, prefer `position: 'auto'` or `position: 'vertical'`.
|
|
171
221
|
`position: 'side'` and `barAnchorPosition: 'top' | 'middle'` are kept for
|
|
172
222
|
legacy configs, but horizontal bars resolve bar anchoring automatically.
|
package/package.json
CHANGED