@internetstiftelsen/charts 0.20.0 → 0.20.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/tooltip/dom.d.ts +3 -0
- package/dist/tooltip/dom.js +19 -0
- package/dist/tooltip/xy-interaction.js +58 -36
- package/docs/components.md +5 -0
- package/package.json +1 -1
package/dist/tooltip/dom.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare class TooltipDom {
|
|
|
27
27
|
setContent(content: string): void;
|
|
28
28
|
getBounds(): DOMRect | null;
|
|
29
29
|
showAt(left: number, top: number): void;
|
|
30
|
+
hasVisibleTooltips(): boolean;
|
|
31
|
+
moveVisibleTooltips(deltaX: number, deltaY: number): void;
|
|
30
32
|
hide(): void;
|
|
31
33
|
cleanup(): void;
|
|
32
34
|
measureTooltip(tooltip: TooltipDivSelection, content: string): {
|
|
@@ -50,6 +52,7 @@ export declare class TooltipDom {
|
|
|
50
52
|
private scheduleTooltipPositionReset;
|
|
51
53
|
private cancelTooltipPositionReset;
|
|
52
54
|
private getTooltipTransitionStyle;
|
|
55
|
+
private getTooltipNodes;
|
|
53
56
|
private isTooltipVisible;
|
|
54
57
|
private getTooltipPosition;
|
|
55
58
|
private hasVisibleSlideOffset;
|
package/dist/tooltip/dom.js
CHANGED
|
@@ -108,6 +108,22 @@ export class TooltipDom {
|
|
|
108
108
|
}
|
|
109
109
|
this.showTooltipAt(this.tooltipDiv, left, top);
|
|
110
110
|
}
|
|
111
|
+
hasVisibleTooltips() {
|
|
112
|
+
return this.getTooltipNodes().some((node) => this.isTooltipVisible(node));
|
|
113
|
+
}
|
|
114
|
+
moveVisibleTooltips(deltaX, deltaY) {
|
|
115
|
+
this.getTooltipNodes().forEach((node) => {
|
|
116
|
+
if (!this.isTooltipVisible(node)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const position = this.getTooltipPosition(node);
|
|
120
|
+
if (!position) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
node.style.left = `${position.left + deltaX}px`;
|
|
124
|
+
node.style.top = `${position.top + deltaY}px`;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
111
127
|
hide() {
|
|
112
128
|
const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
|
|
113
129
|
if (!tooltip.empty()) {
|
|
@@ -350,6 +366,9 @@ export class TooltipDom {
|
|
|
350
366
|
getTooltipTransitionStyle() {
|
|
351
367
|
return `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`;
|
|
352
368
|
}
|
|
369
|
+
getTooltipNodes() {
|
|
370
|
+
return Array.from(document.querySelectorAll(`#${this.id}, [data-chart-tooltip-owner="${this.splitTooltipOwner}"]`));
|
|
371
|
+
}
|
|
353
372
|
isTooltipVisible(node) {
|
|
354
373
|
return (node.style.visibility === 'visible' && node.style.opacity !== '0');
|
|
355
374
|
}
|
|
@@ -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,57 +319,90 @@ 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
347
|
};
|
|
353
|
-
|
|
348
|
+
const tooltipSvgNode = svg.node();
|
|
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
|
+
};
|
|
354
381
|
const hideTooltip = () => {
|
|
355
|
-
|
|
382
|
+
stopPositionTracking();
|
|
356
383
|
dom.hideTooltipSelection(tooltip);
|
|
357
384
|
dom.hideSplitTooltips();
|
|
358
385
|
clearVisualState();
|
|
359
386
|
};
|
|
360
|
-
const
|
|
361
|
-
|
|
387
|
+
const renderTooltip = (request) => {
|
|
388
|
+
if (!tooltipSvgNode) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
activeSvgBounds = getElementTooltipBounds(tooltipSvgNode);
|
|
392
|
+
if (positionTrackingFrame === null) {
|
|
393
|
+
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
394
|
+
}
|
|
362
395
|
if (mode === 'single') {
|
|
363
|
-
|
|
396
|
+
showSingleTooltip(request);
|
|
364
397
|
return;
|
|
365
398
|
}
|
|
366
|
-
activeTooltipRequest = { index: request.index };
|
|
367
399
|
if (mode === 'split') {
|
|
368
400
|
showSplitTooltipAtIndex(request.index);
|
|
369
401
|
return;
|
|
370
402
|
}
|
|
371
403
|
showSharedTooltipAtIndex(request.index);
|
|
372
|
-
}
|
|
404
|
+
};
|
|
405
|
+
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, renderTooltip);
|
|
373
406
|
overlay
|
|
374
407
|
.on('mousemove', (event) => {
|
|
375
408
|
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
@@ -415,18 +448,7 @@ 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);
|
|
@@ -448,12 +470,12 @@ export function attachXYTooltipArea(config) {
|
|
|
448
470
|
event.preventDefault();
|
|
449
471
|
focusTargetNodes[nextIndex].focus();
|
|
450
472
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
473
|
+
const detachScrollListeners = attachTooltipScrollListeners(tooltipSvgNode, updateTooltipPosition);
|
|
474
|
+
return () => {
|
|
475
|
+
detachScrollListeners?.();
|
|
476
|
+
queuedRender.cancel();
|
|
477
|
+
stopPositionTracking();
|
|
478
|
+
};
|
|
457
479
|
}
|
|
458
480
|
function normalizeFormatterValue(value) {
|
|
459
481
|
if (value === null ||
|
package/docs/components.md
CHANGED
|
@@ -167,6 +167,11 @@ paths are omitted when the arrow is already close to its target. For line,
|
|
|
167
167
|
area, and scatter points, side split tooltips spread nearby point targets
|
|
168
168
|
within the connectorless arrow range before drawing connector paths.
|
|
169
169
|
|
|
170
|
+
Visible XY tooltips track their targets on every animation frame, including
|
|
171
|
+
during touch and momentum scrolling. Their initial placement is preserved,
|
|
172
|
+
allowing them to pass beyond the container edges instead of sticking there
|
|
173
|
+
until blur. Tracking stops when the tooltips are hidden or the chart is destroyed.
|
|
174
|
+
|
|
170
175
|
For horizontal bar charts, prefer `position: 'auto'` or `position: 'vertical'`.
|
|
171
176
|
`position: 'side'` and `barAnchorPosition: 'top' | 'middle'` are kept for
|
|
172
177
|
legacy configs, but horizontal bars resolve bar anchoring automatically.
|
package/package.json
CHANGED