@internetstiftelsen/charts 0.16.0 → 0.18.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/README.md +5 -5
- package/dist/index.d.ts +26 -0
- package/dist/index.js +24 -0
- package/dist/tooltip/dom.d.ts +6 -2
- package/dist/tooltip/dom.js +46 -27
- package/dist/tooltip/geometry.d.ts +4 -3
- package/dist/tooltip/geometry.js +496 -34
- package/dist/tooltip/types.d.ts +2 -0
- package/dist/tooltip/xy-interaction.d.ts +2 -1
- package/dist/tooltip/xy-interaction.js +219 -36
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +10 -1
- package/dist/types.d.ts +5 -3
- package/docs/components.md +20 -10
- package/docs/getting-started.md +13 -9
- package/package.json +10 -3
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { pointer, select } from 'd3';
|
|
2
2
|
import { getSeriesColor } from '../types.js';
|
|
3
|
-
import { sanitizeForCSS } from '../utils.js';
|
|
4
|
-
import { getAnchoredTooltipPosition, resolveSharedTooltipTarget, resolveSplitTooltipPositions, resolveSplitTooltipTarget, resolveTooltipArrowEdge, } from './geometry.js';
|
|
3
|
+
import { getContrastTextColor, sanitizeForCSS } from '../utils.js';
|
|
4
|
+
import { clipTooltipAnchorToBounds, getAnchoredTooltipPosition, getSplitTooltipViewportBounds, resolveSharedTooltipTarget, resolveSplitTooltipPositions, resolveSplitTooltipTarget, resolveTooltipArrowEdge, } from './geometry.js';
|
|
5
5
|
export function attachXYTooltipArea(config) {
|
|
6
|
-
const { svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal, categoryScaleType, mode, position, barAnchorPosition, formatter, labelFormatter, customFormatter, dom, } = config;
|
|
6
|
+
const { svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal, categoryScaleType, mode, position, barAnchorPosition, colorMode, formatter, labelFormatter, customFormatter, dom, } = config;
|
|
7
7
|
const tooltip = dom.getRootTooltip();
|
|
8
8
|
if (!tooltip || data.length === 0) {
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
|
+
const resolvedPosition = position === 'auto' ? (isHorizontal ? 'vertical' : 'side') : position;
|
|
12
|
+
const resolvedBarAnchorPosition = isHorizontal ? 'auto' : barAnchorPosition;
|
|
11
13
|
const resolveSeriesValue = config.resolveSeriesValue ??
|
|
12
14
|
((targetSeries, dataPoint) => {
|
|
13
15
|
const rawValue = dataPoint[targetSeries.dataKey];
|
|
@@ -24,6 +26,15 @@ export function attachXYTooltipArea(config) {
|
|
|
24
26
|
const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
|
|
25
27
|
return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
|
|
26
28
|
};
|
|
29
|
+
const getTooltipValueSign = (value) => {
|
|
30
|
+
if (value < 0) {
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
if (value > 0) {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
};
|
|
27
38
|
const buildTooltipLabel = (dataPoint) => {
|
|
28
39
|
const labelValue = dataPoint[xKey];
|
|
29
40
|
return labelFormatter
|
|
@@ -43,8 +54,26 @@ export function attachXYTooltipArea(config) {
|
|
|
43
54
|
return value !== null && value !== undefined;
|
|
44
55
|
});
|
|
45
56
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
57
|
+
const getSeriesTooltipStyle = (currentSeries, dataPoint, index) => {
|
|
58
|
+
if (colorMode !== 'series') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const seriesColor = currentSeries.type === 'bar' && currentSeries.colorAdapter
|
|
62
|
+
? currentSeries.colorAdapter(dataPoint, index)
|
|
63
|
+
: getSeriesColor(currentSeries);
|
|
64
|
+
return {
|
|
65
|
+
background: seriesColor,
|
|
66
|
+
border: seriesColor,
|
|
67
|
+
color: getContrastTextColor(seriesColor),
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const getSharedTooltipStyle = (visibleSeries, dataPoint, index) => {
|
|
71
|
+
if (visibleSeries.length !== 1) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return getSeriesTooltipStyle(visibleSeries[0], dataPoint, index);
|
|
75
|
+
};
|
|
76
|
+
const buildSharedTooltipContent = (dataPoint, visibleSeries) => {
|
|
48
77
|
if (visibleSeries.length === 0) {
|
|
49
78
|
return null;
|
|
50
79
|
}
|
|
@@ -63,7 +92,7 @@ export function attachXYTooltipArea(config) {
|
|
|
63
92
|
return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
|
|
64
93
|
};
|
|
65
94
|
const buildAccessibleLabel = (dataPoint) => {
|
|
66
|
-
const content = buildSharedTooltipContent(dataPoint);
|
|
95
|
+
const content = buildSharedTooltipContent(dataPoint, getVisibleTooltipSeries(dataPoint));
|
|
67
96
|
return content ? stripHtml(content) : buildTooltipLabel(dataPoint);
|
|
68
97
|
};
|
|
69
98
|
const dataPointPositions = data.map((dataPoint) => isHorizontal ? getYPosition(dataPoint) : getXPosition(dataPoint));
|
|
@@ -143,9 +172,11 @@ export function attachXYTooltipArea(config) {
|
|
|
143
172
|
const showSharedTooltipAtIndex = (closestIndex) => {
|
|
144
173
|
const dataPoint = data[closestIndex];
|
|
145
174
|
const dataPointPosition = dataPointPositions[closestIndex];
|
|
175
|
+
const visibleSeries = getVisibleTooltipSeries(dataPoint);
|
|
146
176
|
updateVisualStateAtIndex(closestIndex);
|
|
147
177
|
dom.hideSplitTooltips();
|
|
148
|
-
|
|
178
|
+
dom.applyRootTooltipStyles(theme, getSharedTooltipStyle(visibleSeries, dataPoint, closestIndex));
|
|
179
|
+
const content = buildSharedTooltipContent(dataPoint, visibleSeries);
|
|
149
180
|
if (!content) {
|
|
150
181
|
dom.hideTooltipSelection(tooltip);
|
|
151
182
|
return;
|
|
@@ -171,17 +202,18 @@ export function attachXYTooltipArea(config) {
|
|
|
171
202
|
resolveSeriesValue,
|
|
172
203
|
closestIndex,
|
|
173
204
|
});
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
205
|
+
const placementBounds = getTooltipPlacementBounds(svgNode);
|
|
206
|
+
const visibleAnchor = clipTooltipAnchorToBounds(sharedAnchor, placementBounds);
|
|
207
|
+
const target = resolveSharedTooltipTarget(visibleAnchor);
|
|
208
|
+
const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, visibleAnchor, target, measuredTooltip.width, measuredTooltip.height, placementBounds);
|
|
209
|
+
const tooltipPosition = getAnchoredTooltipPosition(visibleAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
|
|
177
210
|
if (!tooltipPosition) {
|
|
178
211
|
dom.hideTooltipSelection(tooltip);
|
|
179
212
|
return;
|
|
180
213
|
}
|
|
181
214
|
dom.renderTooltipWithoutConnector(tooltip, tooltipPosition.left, tooltipPosition.top);
|
|
182
215
|
};
|
|
183
|
-
const getSeriesTooltipAnchor = (currentSeries,
|
|
184
|
-
const value = resolveSeriesValue(currentSeries, dataPoint, index);
|
|
216
|
+
const getSeriesTooltipAnchor = (currentSeries, index, value) => {
|
|
185
217
|
if (!Number.isFinite(value)) {
|
|
186
218
|
return null;
|
|
187
219
|
}
|
|
@@ -198,6 +230,12 @@ export function attachXYTooltipArea(config) {
|
|
|
198
230
|
};
|
|
199
231
|
const showSplitTooltipAtIndex = (closestIndex) => {
|
|
200
232
|
const dataPoint = data[closestIndex];
|
|
233
|
+
const svgNode = svg.node();
|
|
234
|
+
if (!svgNode) {
|
|
235
|
+
dom.hideSplitTooltips();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const placementBounds = getTooltipPlacementBounds(svgNode);
|
|
201
239
|
updateVisualStateAtIndex(closestIndex);
|
|
202
240
|
dom.hideTooltipSelection(tooltip);
|
|
203
241
|
const layouts = [];
|
|
@@ -206,20 +244,26 @@ export function attachXYTooltipArea(config) {
|
|
|
206
244
|
if (rawValue === null || rawValue === undefined) {
|
|
207
245
|
return;
|
|
208
246
|
}
|
|
209
|
-
const
|
|
247
|
+
const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
|
|
248
|
+
const anchor = getSeriesTooltipAnchor(currentSeries, closestIndex, value);
|
|
210
249
|
if (!anchor) {
|
|
211
250
|
return;
|
|
212
251
|
}
|
|
213
|
-
const
|
|
252
|
+
const visibleAnchor = clipTooltipAnchorToBounds(anchor, placementBounds);
|
|
253
|
+
const splitTooltip = dom.getSplitTooltip(seriesIndex, theme, getSeriesTooltipStyle(currentSeries, dataPoint, closestIndex));
|
|
214
254
|
const content = buildSplitTooltipContent(dataPoint, currentSeries);
|
|
215
255
|
const measuredTooltip = dom.measureTooltip(splitTooltip, content);
|
|
216
256
|
if (!measuredTooltip) {
|
|
217
257
|
return;
|
|
218
258
|
}
|
|
219
|
-
const target = resolveSplitTooltipTarget(currentSeries,
|
|
259
|
+
const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
|
|
260
|
+
const targetMode = currentSeries.type === 'bar' &&
|
|
261
|
+
resolvedBarAnchorPosition === 'auto'
|
|
262
|
+
? 'auto'
|
|
263
|
+
: 'fixed';
|
|
220
264
|
layouts.push({
|
|
221
265
|
div: splitTooltip,
|
|
222
|
-
anchor,
|
|
266
|
+
anchor: visibleAnchor,
|
|
223
267
|
width: measuredTooltip.width,
|
|
224
268
|
height: measuredTooltip.height,
|
|
225
269
|
left: 0,
|
|
@@ -228,39 +272,104 @@ export function attachXYTooltipArea(config) {
|
|
|
228
272
|
targetX: target.x,
|
|
229
273
|
targetY: target.y,
|
|
230
274
|
order: seriesIndex,
|
|
275
|
+
targetMode,
|
|
276
|
+
valueSign: getTooltipValueSign(value),
|
|
231
277
|
});
|
|
232
278
|
});
|
|
233
279
|
if (layouts.length === 0) {
|
|
234
280
|
dom.hideSplitTooltips();
|
|
235
281
|
return;
|
|
236
282
|
}
|
|
237
|
-
resolveSplitTooltipPositions(layouts,
|
|
283
|
+
resolveSplitTooltipPositions(layouts, resolvedPosition, placementBounds, isHorizontal);
|
|
238
284
|
dom.hideUnusedSplitTooltips(layouts.map((layout) => layout.div));
|
|
239
285
|
layouts.forEach((layout) => {
|
|
240
286
|
dom.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
|
|
241
287
|
});
|
|
242
288
|
};
|
|
243
|
-
|
|
289
|
+
const showSingleTooltip = (request) => {
|
|
290
|
+
const dataPoint = data[request.index];
|
|
291
|
+
const svgNode = svg.node();
|
|
292
|
+
if (!svgNode) {
|
|
293
|
+
dom.hideTooltipSelection(tooltip);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const placementBounds = getTooltipPlacementBounds(svgNode);
|
|
297
|
+
const candidates = [];
|
|
298
|
+
series.forEach((currentSeries, seriesIndex) => {
|
|
299
|
+
const rawValue = dataPoint[currentSeries.dataKey];
|
|
300
|
+
if (rawValue === null || rawValue === undefined) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const value = resolveSeriesValue(currentSeries, dataPoint, request.index);
|
|
304
|
+
const anchor = getSeriesTooltipAnchor(currentSeries, request.index, value);
|
|
305
|
+
if (!anchor) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const visibleAnchor = clipTooltipAnchorToBounds(anchor, placementBounds);
|
|
309
|
+
const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
|
|
310
|
+
candidates.push({
|
|
311
|
+
series: currentSeries,
|
|
312
|
+
seriesIndex,
|
|
313
|
+
anchor: visibleAnchor,
|
|
314
|
+
target,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
const selectedCandidate = candidates.find((candidate) => candidate.seriesIndex === request.seriesIndex) ?? getClosestSingleTooltipCandidate(candidates, request);
|
|
318
|
+
updateVisualStateAtIndex(request.index);
|
|
319
|
+
dom.hideSplitTooltips();
|
|
320
|
+
if (!selectedCandidate) {
|
|
321
|
+
dom.hideTooltipSelection(tooltip);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
dom.applyRootTooltipStyles(theme, getSeriesTooltipStyle(selectedCandidate.series, dataPoint, request.index));
|
|
325
|
+
const content = buildSplitTooltipContent(dataPoint, selectedCandidate.series);
|
|
326
|
+
const measuredTooltip = dom.measureTooltip(tooltip, content);
|
|
327
|
+
if (!measuredTooltip) {
|
|
328
|
+
dom.hideTooltipSelection(tooltip);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, placementBounds);
|
|
332
|
+
const tooltipPosition = getAnchoredTooltipPosition(selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
|
|
333
|
+
if (!tooltipPosition) {
|
|
334
|
+
dom.hideTooltipSelection(tooltip);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
dom.renderTooltipWithConnector(tooltip, arrowEdge, tooltipPosition.left, tooltipPosition.top, measuredTooltip.width, measuredTooltip.height, selectedCandidate.target.x, selectedCandidate.target.y, selectedCandidate.anchor);
|
|
338
|
+
return {
|
|
339
|
+
index: request.index,
|
|
340
|
+
seriesIndex: selectedCandidate.seriesIndex,
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
let activeTooltipRequest = null;
|
|
244
344
|
const hideTooltip = () => {
|
|
245
|
-
|
|
345
|
+
activeTooltipRequest = null;
|
|
246
346
|
dom.hideTooltipSelection(tooltip);
|
|
247
347
|
dom.hideSplitTooltips();
|
|
248
348
|
clearVisualState();
|
|
249
349
|
};
|
|
250
350
|
const tooltipSvgNode = svg.node();
|
|
251
|
-
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, (
|
|
252
|
-
|
|
351
|
+
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, (request) => {
|
|
352
|
+
if (mode === 'single') {
|
|
353
|
+
activeTooltipRequest = showSingleTooltip(request);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
activeTooltipRequest = { index: request.index };
|
|
253
357
|
if (mode === 'split') {
|
|
254
|
-
showSplitTooltipAtIndex(index);
|
|
358
|
+
showSplitTooltipAtIndex(request.index);
|
|
255
359
|
return;
|
|
256
360
|
}
|
|
257
|
-
showSharedTooltipAtIndex(index);
|
|
361
|
+
showSharedTooltipAtIndex(request.index);
|
|
258
362
|
});
|
|
259
363
|
overlay
|
|
260
364
|
.on('mousemove', (event) => {
|
|
261
365
|
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
262
366
|
const closestIndex = getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal);
|
|
263
|
-
|
|
367
|
+
const pointerPosition = getDocumentPointerPosition(event);
|
|
368
|
+
queuedRender.request({
|
|
369
|
+
index: closestIndex,
|
|
370
|
+
pointerX: pointerPosition.x,
|
|
371
|
+
pointerY: pointerPosition.y,
|
|
372
|
+
});
|
|
264
373
|
})
|
|
265
374
|
.on('mouseout', () => {
|
|
266
375
|
if (isTooltipFocusTarget(document.activeElement)) {
|
|
@@ -296,7 +405,13 @@ export function attachXYTooltipArea(config) {
|
|
|
296
405
|
}
|
|
297
406
|
queuedRender.cancel();
|
|
298
407
|
select(this).attr('stroke', '#111827').attr('stroke-width', 2);
|
|
299
|
-
|
|
408
|
+
if (mode === 'single') {
|
|
409
|
+
activeTooltipRequest = showSingleTooltip({
|
|
410
|
+
index: currentIndex,
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
activeTooltipRequest = { index: currentIndex };
|
|
300
415
|
if (mode === 'split') {
|
|
301
416
|
showSplitTooltipAtIndex(currentIndex);
|
|
302
417
|
return;
|
|
@@ -324,10 +439,10 @@ export function attachXYTooltipArea(config) {
|
|
|
324
439
|
focusTargetNodes[nextIndex].focus();
|
|
325
440
|
});
|
|
326
441
|
return attachTooltipScrollListeners(tooltipSvgNode, () => {
|
|
327
|
-
if (
|
|
442
|
+
if (activeTooltipRequest === null) {
|
|
328
443
|
return;
|
|
329
444
|
}
|
|
330
|
-
queuedRender.request(
|
|
445
|
+
queuedRender.request(activeTooltipRequest);
|
|
331
446
|
});
|
|
332
447
|
}
|
|
333
448
|
function normalizeFormatterValue(value) {
|
|
@@ -376,6 +491,33 @@ function getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizo
|
|
|
376
491
|
}
|
|
377
492
|
return closestIndex;
|
|
378
493
|
}
|
|
494
|
+
function getDocumentPointerPosition(event) {
|
|
495
|
+
return {
|
|
496
|
+
x: event.clientX + window.scrollX,
|
|
497
|
+
y: event.clientY + window.scrollY,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function getClosestSingleTooltipCandidate(candidates, request) {
|
|
501
|
+
if (candidates.length === 0) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
if (request.pointerX === undefined ||
|
|
505
|
+
request.pointerY === undefined ||
|
|
506
|
+
!Number.isFinite(request.pointerX) ||
|
|
507
|
+
!Number.isFinite(request.pointerY)) {
|
|
508
|
+
return candidates[0];
|
|
509
|
+
}
|
|
510
|
+
return candidates.reduce((closest, candidate) => {
|
|
511
|
+
const closestDistance = getSingleTooltipDistance(closest, request);
|
|
512
|
+
const candidateDistance = getSingleTooltipDistance(candidate, request);
|
|
513
|
+
return candidateDistance < closestDistance ? candidate : closest;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
function getSingleTooltipDistance(candidate, request) {
|
|
517
|
+
const deltaX = (request.pointerX ?? 0) - candidate.target.x;
|
|
518
|
+
const deltaY = (request.pointerY ?? 0) - candidate.target.y;
|
|
519
|
+
return Math.hypot(deltaX, deltaY);
|
|
520
|
+
}
|
|
379
521
|
function resolveSharedTooltipAnchor({ svgNode, dataPoint, dataPointPosition, series, x, y, isHorizontal, resolveSeriesValue, closestIndex, }) {
|
|
380
522
|
const svgRect = svgNode.getBoundingClientRect();
|
|
381
523
|
const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
|
|
@@ -467,25 +609,25 @@ function resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valu
|
|
|
467
609
|
centerY: anchorY,
|
|
468
610
|
};
|
|
469
611
|
}
|
|
470
|
-
function createQueuedTooltipRender(tooltipSvgNode,
|
|
612
|
+
function createQueuedTooltipRender(tooltipSvgNode, renderTooltip) {
|
|
471
613
|
let pendingTooltipFrame = null;
|
|
472
|
-
let
|
|
473
|
-
const request = (
|
|
474
|
-
|
|
614
|
+
let pendingTooltipRequest = null;
|
|
615
|
+
const request = (nextRequest) => {
|
|
616
|
+
pendingTooltipRequest = nextRequest;
|
|
475
617
|
if (pendingTooltipFrame !== null) {
|
|
476
618
|
return;
|
|
477
619
|
}
|
|
478
620
|
pendingTooltipFrame = requestFrame(() => {
|
|
479
|
-
const
|
|
621
|
+
const currentRequest = pendingTooltipRequest;
|
|
480
622
|
pendingTooltipFrame = null;
|
|
481
|
-
|
|
482
|
-
if (
|
|
623
|
+
pendingTooltipRequest = null;
|
|
624
|
+
if (currentRequest === null) {
|
|
483
625
|
return;
|
|
484
626
|
}
|
|
485
627
|
if (!tooltipSvgNode?.isConnected) {
|
|
486
628
|
return;
|
|
487
629
|
}
|
|
488
|
-
|
|
630
|
+
renderTooltip(currentRequest);
|
|
489
631
|
});
|
|
490
632
|
};
|
|
491
633
|
const cancel = () => {
|
|
@@ -494,7 +636,7 @@ function createQueuedTooltipRender(tooltipSvgNode, renderTooltipAtIndex) {
|
|
|
494
636
|
}
|
|
495
637
|
cancelFrame(pendingTooltipFrame);
|
|
496
638
|
pendingTooltipFrame = null;
|
|
497
|
-
|
|
639
|
+
pendingTooltipRequest = null;
|
|
498
640
|
};
|
|
499
641
|
return { request, cancel };
|
|
500
642
|
}
|
|
@@ -577,6 +719,47 @@ function getNextFocusTargetIndex(currentIndex, key, focusTargetCount) {
|
|
|
577
719
|
return -1;
|
|
578
720
|
}
|
|
579
721
|
}
|
|
722
|
+
function getTooltipPlacementBounds(svgNode) {
|
|
723
|
+
let bounds = getSplitTooltipViewportBounds();
|
|
724
|
+
let currentElement = svgNode.parentElement;
|
|
725
|
+
while (currentElement) {
|
|
726
|
+
if (isScrollableElement(currentElement)) {
|
|
727
|
+
bounds = intersectTooltipBounds(bounds, getElementTooltipBounds(currentElement));
|
|
728
|
+
}
|
|
729
|
+
currentElement = currentElement.parentElement;
|
|
730
|
+
}
|
|
731
|
+
return bounds;
|
|
732
|
+
}
|
|
733
|
+
function getElementTooltipBounds(element) {
|
|
734
|
+
const rect = element.getBoundingClientRect();
|
|
735
|
+
const left = rect.left + window.scrollX;
|
|
736
|
+
const top = rect.top + window.scrollY;
|
|
737
|
+
return {
|
|
738
|
+
minLeft: left,
|
|
739
|
+
maxRight: left + rect.width,
|
|
740
|
+
minTop: top,
|
|
741
|
+
maxBottom: top + rect.height,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function intersectTooltipBounds(bounds, nextBounds) {
|
|
745
|
+
if (nextBounds.maxRight <= nextBounds.minLeft ||
|
|
746
|
+
nextBounds.maxBottom <= nextBounds.minTop) {
|
|
747
|
+
return bounds;
|
|
748
|
+
}
|
|
749
|
+
const minLeft = Math.max(bounds.minLeft, nextBounds.minLeft);
|
|
750
|
+
const maxRight = Math.min(bounds.maxRight, nextBounds.maxRight);
|
|
751
|
+
const minTop = Math.max(bounds.minTop, nextBounds.minTop);
|
|
752
|
+
const maxBottom = Math.min(bounds.maxBottom, nextBounds.maxBottom);
|
|
753
|
+
if (maxRight <= minLeft || maxBottom <= minTop) {
|
|
754
|
+
return bounds;
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
minLeft,
|
|
758
|
+
maxRight,
|
|
759
|
+
minTop,
|
|
760
|
+
maxBottom,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
580
763
|
function attachTooltipScrollListeners(svgNode, onScroll) {
|
|
581
764
|
if (!svgNode) {
|
|
582
765
|
return null;
|
package/dist/tooltip.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Selection } from 'd3';
|
|
2
|
-
import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase, ScaleType, TooltipMode, TooltipPosition, TooltipBarAnchorPosition, TooltipTransitionConfig } from './types.js';
|
|
2
|
+
import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase, ScaleType, TooltipMode, TooltipPosition, TooltipBarAnchorPosition, TooltipTransitionConfig, TooltipColorMode } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
import type { PlotAreaBounds } from './layout-manager.js';
|
|
5
5
|
import { type XYTooltipSeries } from './tooltip/types.js';
|
|
@@ -10,6 +10,7 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
10
10
|
readonly mode: TooltipMode;
|
|
11
11
|
readonly position: TooltipPosition;
|
|
12
12
|
readonly barAnchorPosition: TooltipBarAnchorPosition;
|
|
13
|
+
readonly colorMode: TooltipColorMode;
|
|
13
14
|
readonly maxWidth: number;
|
|
14
15
|
readonly transition: Required<TooltipTransitionConfig>;
|
|
15
16
|
readonly formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
package/dist/tooltip.js
CHANGED
|
@@ -34,6 +34,12 @@ export class Tooltip {
|
|
|
34
34
|
writable: true,
|
|
35
35
|
value: void 0
|
|
36
36
|
});
|
|
37
|
+
Object.defineProperty(this, "colorMode", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: void 0
|
|
42
|
+
});
|
|
37
43
|
Object.defineProperty(this, "maxWidth", {
|
|
38
44
|
enumerable: true,
|
|
39
45
|
configurable: true,
|
|
@@ -82,12 +88,13 @@ export class Tooltip {
|
|
|
82
88
|
writable: true,
|
|
83
89
|
value: null
|
|
84
90
|
});
|
|
85
|
-
const { mode = 'split', position = '
|
|
91
|
+
const { mode = 'split', position = 'auto', barAnchorPosition = 'auto', colorMode = 'theme', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
|
|
86
92
|
const tooltipId = Tooltip.nextTooltipId++;
|
|
87
93
|
this.id = `iisChartTooltip-${tooltipId}`;
|
|
88
94
|
this.mode = mode;
|
|
89
95
|
this.position = position;
|
|
90
96
|
this.barAnchorPosition = barAnchorPosition;
|
|
97
|
+
this.colorMode = colorMode;
|
|
91
98
|
this.maxWidth =
|
|
92
99
|
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
93
100
|
? maxWidth
|
|
@@ -112,6 +119,7 @@ export class Tooltip {
|
|
|
112
119
|
mode: this.mode,
|
|
113
120
|
position: this.position,
|
|
114
121
|
barAnchorPosition: this.barAnchorPosition,
|
|
122
|
+
colorMode: this.colorMode,
|
|
115
123
|
maxWidth: this.maxWidth,
|
|
116
124
|
transition: this.transition,
|
|
117
125
|
formatter: this.formatter,
|
|
@@ -153,6 +161,7 @@ export class Tooltip {
|
|
|
153
161
|
mode: this.mode,
|
|
154
162
|
position: this.position,
|
|
155
163
|
barAnchorPosition: this.barAnchorPosition,
|
|
164
|
+
colorMode: this.colorMode,
|
|
156
165
|
formatter: this.formatter,
|
|
157
166
|
labelFormatter: this.labelFormatter,
|
|
158
167
|
customFormatter: this.customFormatter,
|
package/dist/types.d.ts
CHANGED
|
@@ -299,9 +299,10 @@ export type GridConfigBase = {
|
|
|
299
299
|
export type GridConfig = GridConfigBase & {
|
|
300
300
|
exportHooks?: ExportHooks<GridConfigBase>;
|
|
301
301
|
};
|
|
302
|
-
export type TooltipMode = 'shared' | 'split';
|
|
303
|
-
export type TooltipPosition = 'side' | 'vertical';
|
|
304
|
-
export type TooltipBarAnchorPosition = 'top' | 'middle';
|
|
302
|
+
export type TooltipMode = 'shared' | 'split' | 'single';
|
|
303
|
+
export type TooltipPosition = 'auto' | 'side' | 'vertical';
|
|
304
|
+
export type TooltipBarAnchorPosition = 'auto' | 'top' | 'middle';
|
|
305
|
+
export type TooltipColorMode = 'theme' | 'series';
|
|
305
306
|
export type TooltipTransitionConfig = {
|
|
306
307
|
show?: boolean;
|
|
307
308
|
duration?: number;
|
|
@@ -311,6 +312,7 @@ export type TooltipConfigBase = {
|
|
|
311
312
|
mode?: TooltipMode;
|
|
312
313
|
position?: TooltipPosition;
|
|
313
314
|
barAnchorPosition?: TooltipBarAnchorPosition;
|
|
315
|
+
colorMode?: TooltipColorMode;
|
|
314
316
|
maxWidth?: number;
|
|
315
317
|
transition?: TooltipTransitionConfig;
|
|
316
318
|
formatter?: SeriesValueFormatter;
|
package/docs/components.md
CHANGED
|
@@ -125,9 +125,10 @@ Renders interactive tooltips on hover and keyboard focus.
|
|
|
125
125
|
|
|
126
126
|
```typescript
|
|
127
127
|
new Tooltip({
|
|
128
|
-
mode?: 'shared' | 'split',
|
|
129
|
-
position?: 'side' | 'vertical',
|
|
130
|
-
barAnchorPosition?: 'top' | 'middle',
|
|
128
|
+
mode?: 'shared' | 'split' | 'single',
|
|
129
|
+
position?: 'auto' | 'side' | 'vertical',
|
|
130
|
+
barAnchorPosition?: 'auto' | 'top' | 'middle',
|
|
131
|
+
colorMode?: 'theme' | 'series',
|
|
131
132
|
maxWidth?: number, // default: 280
|
|
132
133
|
transition?: {
|
|
133
134
|
show?: boolean,
|
|
@@ -146,18 +147,27 @@ The formatter receives:
|
|
|
146
147
|
|
|
147
148
|
Tooltip modes:
|
|
148
149
|
|
|
149
|
-
- `shared` - One tooltip per hovered category/value group
|
|
150
150
|
- `split` - Default. One tooltip per visible series at the hovered category/value group
|
|
151
|
+
- `single` - One tooltip for the closest visible series at the hovered category/value group
|
|
152
|
+
- `shared` - One grouped tooltip per hovered category/value group
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
data point.
|
|
154
|
+
Single and shared tooltips omit series whose value is `null` or `undefined` for
|
|
155
|
+
the hovered data point. `customFormatter` receives the current series in
|
|
156
|
+
`split` and `single` modes, and the visible series list in `shared` mode.
|
|
154
157
|
|
|
155
|
-
Use `position: 'side' | 'vertical'` for
|
|
156
|
-
`
|
|
157
|
-
|
|
158
|
+
Use `position: 'auto' | 'side' | 'vertical'` for XY tooltip placement.
|
|
159
|
+
`auto` is the default. It uses above/below placement for horizontal XY charts
|
|
160
|
+
and side placement elsewhere. For bar tooltips, `barAnchorPosition` defaults to
|
|
161
|
+
`auto`, which aims arrows inside the visible bar segment when possible.
|
|
162
|
+
|
|
163
|
+
For horizontal bar charts, prefer `position: 'auto'` or `position: 'vertical'`.
|
|
164
|
+
`position: 'side'` and `barAnchorPosition: 'top' | 'middle'` are kept for
|
|
165
|
+
legacy configs, but horizontal bars resolve bar anchoring automatically.
|
|
158
166
|
|
|
159
167
|
Tooltips default to a `280px` max width. Set `transition.show: true` to fade and
|
|
160
|
-
slide tooltips between hovered positions. Tooltip colors use `theme.tooltip
|
|
168
|
+
slide tooltips between hovered positions. Tooltip colors use `theme.tooltip` by
|
|
169
|
+
default. Set `colorMode: 'series'` to color-code XY tooltips from the series
|
|
170
|
+
color, with matching background and border colors plus automatic contrast text.
|
|
161
171
|
|
|
162
172
|
`defaultResponsiveConfig` switches tooltip components to `mode: 'shared'` at
|
|
163
173
|
the `sm` breakpoint so compact XY charts use one grouped tooltip by default.
|
package/docs/getting-started.md
CHANGED
|
@@ -11,15 +11,16 @@ npm install @internetstiftelsen/charts
|
|
|
11
11
|
## Vanilla JavaScript
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
import {
|
|
15
|
+
Grid,
|
|
16
|
+
Legend,
|
|
17
|
+
Line,
|
|
18
|
+
Title,
|
|
19
|
+
Tooltip,
|
|
20
|
+
XAxis,
|
|
21
|
+
XYChart,
|
|
22
|
+
YAxis,
|
|
23
|
+
} from '@internetstiftelsen/charts';
|
|
23
24
|
|
|
24
25
|
// Your data
|
|
25
26
|
const data = [
|
|
@@ -57,6 +58,9 @@ chart.update(newData);
|
|
|
57
58
|
chart.destroy();
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
Subpath imports like `@internetstiftelsen/charts/xy-chart` remain available when
|
|
62
|
+
you want to import individual modules directly.
|
|
63
|
+
|
|
60
64
|
## Lifecycle Events
|
|
61
65
|
|
|
62
66
|
Charts support lifecycle listeners with `on()` and `off()`.
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.18.0",
|
|
3
3
|
"name": "@internetstiftelsen/charts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"main": "./dist/index.js",
|
|
6
9
|
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json",
|
|
7
15
|
"./*": {
|
|
8
16
|
"types": "./dist/*.d.ts",
|
|
9
17
|
"import": "./dist/*.js"
|
|
10
|
-
}
|
|
11
|
-
"./package.json": "./package.json"
|
|
18
|
+
}
|
|
12
19
|
},
|
|
13
20
|
"files": [
|
|
14
21
|
"dist",
|