@internetstiftelsen/charts 0.15.0 → 0.16.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 +4 -0
- package/dist/theme.js +6 -0
- package/dist/tooltip/dom.d.ts +56 -0
- package/dist/tooltip/dom.js +438 -0
- package/dist/tooltip/geometry.d.ts +18 -0
- package/dist/tooltip/geometry.js +395 -0
- package/dist/tooltip/types.d.ts +77 -0
- package/dist/tooltip/types.js +24 -0
- package/dist/tooltip/xy-interaction.d.ts +33 -0
- package/dist/tooltip/xy-interaction.js +608 -0
- package/dist/tooltip.d.ts +4 -74
- package/dist/tooltip.js +41 -1444
- package/dist/types.d.ts +2 -0
- package/dist/x-axis.d.ts +11 -1
- package/dist/x-axis.js +150 -10
- package/dist/xy-chart.d.ts +1 -0
- package/dist/xy-chart.js +10 -4
- package/docs/components.md +16 -19
- package/docs/xy-chart.md +3 -10
- package/package.json +1 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { SPLIT_TOOLTIP_GAP_PX, TOOLTIP_BOX_ARROW_LENGTH_PX, TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX, TOOLTIP_CONNECTOR_ELBOW_RATIO, TOOLTIP_CONNECTOR_INSET_PX, TOOLTIP_CONNECTOR_PADDING_PX, TOOLTIP_OFFSET_PX, TOOLTIP_TOTAL_BORDER_WIDTH_PX, TOOLTIP_VIEWPORT_PADDING_PX, } from './types.js';
|
|
2
|
+
export function getSplitTooltipViewportBounds() {
|
|
3
|
+
const visualViewport = window.visualViewport;
|
|
4
|
+
const viewportLeft = window.scrollX + (visualViewport?.offsetLeft ?? 0);
|
|
5
|
+
const viewportTop = window.scrollY + (visualViewport?.offsetTop ?? 0);
|
|
6
|
+
const viewportWidth = visualViewport?.width ?? window.innerWidth;
|
|
7
|
+
const viewportHeight = visualViewport?.height ?? window.innerHeight;
|
|
8
|
+
return {
|
|
9
|
+
minLeft: viewportLeft + TOOLTIP_VIEWPORT_PADDING_PX,
|
|
10
|
+
maxRight: viewportLeft + viewportWidth - TOOLTIP_VIEWPORT_PADDING_PX,
|
|
11
|
+
minTop: viewportTop + TOOLTIP_VIEWPORT_PADDING_PX,
|
|
12
|
+
maxBottom: viewportTop + viewportHeight - TOOLTIP_VIEWPORT_PADDING_PX,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function resolveTooltipArrowEdge(position, anchor, target, tooltipWidth, tooltipHeight) {
|
|
16
|
+
if (position === 'vertical') {
|
|
17
|
+
return resolveVerticalPlacementArrowEdge(target, tooltipHeight);
|
|
18
|
+
}
|
|
19
|
+
return resolveSidePlacementArrowEdge(anchor, tooltipWidth);
|
|
20
|
+
}
|
|
21
|
+
export function resolveSidePlacementArrowEdge(anchor, tooltipWidth, bounds = getSplitTooltipViewportBounds()) {
|
|
22
|
+
const availableRightSpace = bounds.maxRight - anchor.right - TOOLTIP_OFFSET_PX;
|
|
23
|
+
const availableLeftSpace = anchor.left - bounds.minLeft - TOOLTIP_OFFSET_PX;
|
|
24
|
+
if (availableRightSpace >= tooltipWidth ||
|
|
25
|
+
availableRightSpace >= availableLeftSpace) {
|
|
26
|
+
return 'left';
|
|
27
|
+
}
|
|
28
|
+
return 'right';
|
|
29
|
+
}
|
|
30
|
+
export function resolveVerticalPlacementArrowEdge(target, tooltipHeight, bounds = getSplitTooltipViewportBounds()) {
|
|
31
|
+
const availableTopSpace = target.y - bounds.minTop - TOOLTIP_OFFSET_PX;
|
|
32
|
+
const availableBottomSpace = bounds.maxBottom - target.y - TOOLTIP_OFFSET_PX;
|
|
33
|
+
if (availableTopSpace >= tooltipHeight ||
|
|
34
|
+
availableTopSpace >= availableBottomSpace) {
|
|
35
|
+
return 'bottom';
|
|
36
|
+
}
|
|
37
|
+
return 'top';
|
|
38
|
+
}
|
|
39
|
+
export function resolveSharedTooltipTarget(anchor) {
|
|
40
|
+
return {
|
|
41
|
+
x: anchor.centerX,
|
|
42
|
+
y: anchor.centerY,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function resolveSplitTooltipTarget(currentSeries, anchor, barAnchorPosition) {
|
|
46
|
+
if (currentSeries.type === 'bar') {
|
|
47
|
+
return {
|
|
48
|
+
x: anchor.centerX,
|
|
49
|
+
y: barAnchorPosition === 'top' ? anchor.top : anchor.centerY,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
x: anchor.centerX,
|
|
54
|
+
y: anchor.centerY,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function getAnchoredTooltipPosition(anchor, target, tooltipWidth, tooltipHeight, arrowEdge) {
|
|
58
|
+
const bounds = getSplitTooltipViewportBounds();
|
|
59
|
+
let left = target.x - tooltipWidth / 2;
|
|
60
|
+
let top = target.y - tooltipHeight / 2;
|
|
61
|
+
if (arrowEdge === 'left') {
|
|
62
|
+
left = anchor.right + TOOLTIP_OFFSET_PX;
|
|
63
|
+
}
|
|
64
|
+
else if (arrowEdge === 'right') {
|
|
65
|
+
left = anchor.left - tooltipWidth - TOOLTIP_OFFSET_PX;
|
|
66
|
+
}
|
|
67
|
+
else if (arrowEdge === 'bottom') {
|
|
68
|
+
top = target.y - tooltipHeight - TOOLTIP_OFFSET_PX;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
top = target.y + TOOLTIP_OFFSET_PX;
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isFinite(left) || !Number.isFinite(top)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
left: clampTooltipCoordinate(left, bounds.minLeft, bounds.maxRight - tooltipWidth),
|
|
78
|
+
top: clampTooltipCoordinate(top, bounds.minTop, bounds.maxBottom - tooltipHeight),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
|
|
82
|
+
const localTargetX = targetX - tooltipLeft;
|
|
83
|
+
const localTargetY = targetY - tooltipTop;
|
|
84
|
+
if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const boxArrowPosition = resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
88
|
+
const arrowTip = resolveTooltipArrowTip(arrowEdge, boxArrowPosition.x, boxArrowPosition.y, TOOLTIP_BOX_ARROW_LENGTH_PX);
|
|
89
|
+
const minX = Math.min(arrowTip.x, localTargetX) - TOOLTIP_CONNECTOR_PADDING_PX;
|
|
90
|
+
const maxX = Math.max(arrowTip.x, localTargetX) + TOOLTIP_CONNECTOR_PADDING_PX;
|
|
91
|
+
const minY = Math.min(arrowTip.y, localTargetY) - TOOLTIP_CONNECTOR_PADDING_PX;
|
|
92
|
+
const maxY = Math.max(arrowTip.y, localTargetY) + TOOLTIP_CONNECTOR_PADDING_PX;
|
|
93
|
+
const width = Math.max(1, maxX - minX);
|
|
94
|
+
const height = Math.max(1, maxY - minY);
|
|
95
|
+
const boxX = boxArrowPosition.x - minX;
|
|
96
|
+
const boxY = boxArrowPosition.y - minY;
|
|
97
|
+
const startX = arrowTip.x - minX;
|
|
98
|
+
const startY = arrowTip.y - minY;
|
|
99
|
+
const endX = localTargetX - minX;
|
|
100
|
+
const endY = localTargetY - minY;
|
|
101
|
+
const arrowTipX = tooltipLeft + arrowTip.x;
|
|
102
|
+
const arrowTipY = tooltipTop + arrowTip.y;
|
|
103
|
+
const connectorPath = resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor);
|
|
104
|
+
if (!hasFiniteNumbers(width, height, boxX, boxY, startX, startY, endX, endY)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
left: minX,
|
|
109
|
+
top: minY,
|
|
110
|
+
arrowEdge,
|
|
111
|
+
width,
|
|
112
|
+
height,
|
|
113
|
+
path: connectorPath,
|
|
114
|
+
arrowX: boxArrowPosition.x,
|
|
115
|
+
arrowY: boxArrowPosition.y,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function resolveTooltipArrowPosition(arrowEdge, boxX, boxY, length, halfHeight) {
|
|
119
|
+
switch (arrowEdge) {
|
|
120
|
+
case 'left':
|
|
121
|
+
return {
|
|
122
|
+
left: boxX - length,
|
|
123
|
+
top: boxY - halfHeight,
|
|
124
|
+
};
|
|
125
|
+
case 'right':
|
|
126
|
+
return {
|
|
127
|
+
left: boxX,
|
|
128
|
+
top: boxY - halfHeight,
|
|
129
|
+
};
|
|
130
|
+
case 'top':
|
|
131
|
+
return {
|
|
132
|
+
left: boxX - halfHeight,
|
|
133
|
+
top: boxY - length,
|
|
134
|
+
};
|
|
135
|
+
case 'bottom':
|
|
136
|
+
return {
|
|
137
|
+
left: boxX - halfHeight,
|
|
138
|
+
top: boxY,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function resolveSplitTooltipPositions(layouts, position) {
|
|
143
|
+
if (layouts.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const bounds = getSplitTooltipViewportBounds();
|
|
147
|
+
if (position === 'vertical') {
|
|
148
|
+
resolveAboveBelowSplitTooltipPositions(layouts, bounds);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const arrowEdge = resolveSidePlacementArrowEdge(getCombinedSplitTooltipAnchor(layouts), Math.max(...layouts.map((layout) => layout.width)), bounds);
|
|
152
|
+
const orderedLayouts = getVerticallyOrderedSplitTooltipLayouts(layouts);
|
|
153
|
+
positionSideSplitTooltipStack(orderedLayouts, arrowEdge, bounds);
|
|
154
|
+
}
|
|
155
|
+
function resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
156
|
+
const rightInnerBorderX = tooltipWidth - TOOLTIP_TOTAL_BORDER_WIDTH_PX;
|
|
157
|
+
const bottomInnerBorderY = tooltipHeight - TOOLTIP_TOTAL_BORDER_WIDTH_PX;
|
|
158
|
+
switch (arrowEdge) {
|
|
159
|
+
case 'left':
|
|
160
|
+
return {
|
|
161
|
+
x: 0,
|
|
162
|
+
y: getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
|
|
163
|
+
};
|
|
164
|
+
case 'right':
|
|
165
|
+
return {
|
|
166
|
+
x: rightInnerBorderX,
|
|
167
|
+
y: getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
|
|
168
|
+
};
|
|
169
|
+
case 'top':
|
|
170
|
+
return {
|
|
171
|
+
x: getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
|
|
172
|
+
y: 0,
|
|
173
|
+
};
|
|
174
|
+
case 'bottom':
|
|
175
|
+
return {
|
|
176
|
+
x: getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
|
|
177
|
+
y: bottomInnerBorderY,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function getTooltipConnectorOffset(start, size, target) {
|
|
182
|
+
const minOffset = TOOLTIP_CONNECTOR_INSET_PX;
|
|
183
|
+
const maxOffset = Math.max(minOffset, size - minOffset);
|
|
184
|
+
const preferredOffset = target - start;
|
|
185
|
+
return Math.max(minOffset, Math.min(preferredOffset, maxOffset));
|
|
186
|
+
}
|
|
187
|
+
function resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
|
|
188
|
+
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
189
|
+
if (isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
193
|
+
return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
|
|
194
|
+
}
|
|
195
|
+
if (isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor)) {
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
199
|
+
return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
|
|
200
|
+
}
|
|
201
|
+
function isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor) {
|
|
202
|
+
return (arrowTipX >= anchor.left - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
203
|
+
arrowTipX <= anchor.right + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
204
|
+
}
|
|
205
|
+
function isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor) {
|
|
206
|
+
return (arrowTipY >= anchor.top - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
207
|
+
arrowTipY <= anchor.bottom + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
208
|
+
}
|
|
209
|
+
function resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
|
|
210
|
+
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
211
|
+
return {
|
|
212
|
+
x: arrowEdge === 'left' ? boxX - length : boxX + length,
|
|
213
|
+
y: boxY,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
x: boxX,
|
|
218
|
+
y: arrowEdge === 'top' ? boxY - length : boxY + length,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function getVerticallyOrderedSplitTooltipLayouts(layouts) {
|
|
222
|
+
return [...layouts].sort((a, b) => {
|
|
223
|
+
return (a.targetY - b.targetY || a.targetX - b.targetX || a.order - b.order);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function getCombinedSplitTooltipAnchor(layouts) {
|
|
227
|
+
const left = Math.min(...layouts.map((layout) => layout.anchor.left));
|
|
228
|
+
const right = Math.max(...layouts.map((layout) => layout.anchor.right));
|
|
229
|
+
const top = Math.min(...layouts.map((layout) => layout.anchor.top));
|
|
230
|
+
const bottom = Math.max(...layouts.map((layout) => layout.anchor.bottom));
|
|
231
|
+
return {
|
|
232
|
+
left,
|
|
233
|
+
right,
|
|
234
|
+
top,
|
|
235
|
+
bottom,
|
|
236
|
+
centerX: (left + right) / 2,
|
|
237
|
+
centerY: (top + bottom) / 2,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function getCombinedSplitTooltipTarget(layouts) {
|
|
241
|
+
const minX = Math.min(...layouts.map((layout) => layout.targetX));
|
|
242
|
+
const maxX = Math.max(...layouts.map((layout) => layout.targetX));
|
|
243
|
+
const minY = Math.min(...layouts.map((layout) => layout.targetY));
|
|
244
|
+
const maxY = Math.max(...layouts.map((layout) => layout.targetY));
|
|
245
|
+
return {
|
|
246
|
+
x: (minX + maxX) / 2,
|
|
247
|
+
y: (minY + maxY) / 2,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function positionSideSplitTooltipStack(layouts, arrowEdge, bounds) {
|
|
251
|
+
const anchor = getCombinedSplitTooltipAnchor(layouts);
|
|
252
|
+
const maxWidth = Math.max(...layouts.map((layout) => layout.width));
|
|
253
|
+
const preferredLeft = arrowEdge === 'left'
|
|
254
|
+
? anchor.right + TOOLTIP_OFFSET_PX
|
|
255
|
+
: anchor.left - maxWidth - TOOLTIP_OFFSET_PX;
|
|
256
|
+
const stackLeft = clampTooltipCoordinate(preferredLeft, bounds.minLeft, bounds.maxRight - maxWidth);
|
|
257
|
+
const placedLayouts = [];
|
|
258
|
+
layouts.forEach((layout) => {
|
|
259
|
+
layout.arrowEdge = arrowEdge;
|
|
260
|
+
layout.left =
|
|
261
|
+
arrowEdge === 'left'
|
|
262
|
+
? stackLeft
|
|
263
|
+
: stackLeft + maxWidth - layout.width;
|
|
264
|
+
positionSideSplitTooltip(layout, placedLayouts, bounds);
|
|
265
|
+
placedLayouts.push(layout);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function positionSideSplitTooltip(layout, placedLayouts, bounds) {
|
|
269
|
+
const candidates = getSidePlacementCandidates(layout, placedLayouts, bounds);
|
|
270
|
+
const placement = candidates.find((candidate) => isSplitTooltipCandidateWithinBounds(candidate, layout.height, bounds) &&
|
|
271
|
+
!doesSplitTooltipVerticalCandidateOverlap(candidate, layout.height, placedLayouts)) ?? candidates[0];
|
|
272
|
+
layout.top = clampTooltipCoordinate(placement.top, bounds.minTop, bounds.maxBottom - layout.height);
|
|
273
|
+
}
|
|
274
|
+
function getSidePlacementCandidates(layout, placedLayouts, bounds) {
|
|
275
|
+
const candidates = [
|
|
276
|
+
{
|
|
277
|
+
top: layout.targetY - layout.height / 2,
|
|
278
|
+
arrowEdge: layout.arrowEdge,
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
placedLayouts.forEach((placedLayout) => {
|
|
282
|
+
candidates.push({
|
|
283
|
+
top: placedLayout.top - layout.height - SPLIT_TOOLTIP_GAP_PX,
|
|
284
|
+
arrowEdge: layout.arrowEdge,
|
|
285
|
+
});
|
|
286
|
+
candidates.push({
|
|
287
|
+
top: placedLayout.top + placedLayout.height + SPLIT_TOOLTIP_GAP_PX,
|
|
288
|
+
arrowEdge: layout.arrowEdge,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
return candidates
|
|
292
|
+
.map((candidate) => ({
|
|
293
|
+
...candidate,
|
|
294
|
+
top: clampTooltipCoordinate(candidate.top, bounds.minTop, bounds.maxBottom - layout.height),
|
|
295
|
+
}))
|
|
296
|
+
.sort((a, b) => getSidePlacementCost(a, layout) -
|
|
297
|
+
getSidePlacementCost(b, layout));
|
|
298
|
+
}
|
|
299
|
+
function getSidePlacementCost(candidate, layout) {
|
|
300
|
+
return Math.abs(layout.targetY - (candidate.top + layout.height / 2));
|
|
301
|
+
}
|
|
302
|
+
function resolveAboveBelowSplitTooltipPositions(layouts, bounds) {
|
|
303
|
+
const orderedLayouts = getVerticallyOrderedSplitTooltipLayouts(layouts);
|
|
304
|
+
const target = getCombinedSplitTooltipTarget(layouts);
|
|
305
|
+
const maxWidth = Math.max(...layouts.map((layout) => layout.width));
|
|
306
|
+
const columnLeft = clampTooltipCoordinate(target.x - maxWidth / 2, bounds.minLeft, bounds.maxRight - maxWidth);
|
|
307
|
+
const placedLayouts = [];
|
|
308
|
+
orderedLayouts.forEach((layout) => {
|
|
309
|
+
layout.left = columnLeft + (maxWidth - layout.width) / 2;
|
|
310
|
+
positionAboveBelowSplitTooltip(layout, placedLayouts, bounds);
|
|
311
|
+
placedLayouts.push(layout);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function positionAboveBelowSplitTooltip(layout, placedLayouts, bounds) {
|
|
315
|
+
const preferredEdge = resolveVerticalPlacementArrowEdge({ x: layout.targetX, y: layout.targetY }, layout.height, bounds);
|
|
316
|
+
const candidates = getAboveBelowPlacementCandidates(layout, placedLayouts, bounds, preferredEdge);
|
|
317
|
+
const placement = candidates.find((candidate) => isAboveBelowCandidateOnArrowSide(candidate, layout) &&
|
|
318
|
+
isSplitTooltipCandidateWithinBounds(candidate, layout.height, bounds) &&
|
|
319
|
+
!doesSplitTooltipVerticalCandidateOverlap(candidate, layout.height, placedLayouts)) ?? candidates[0];
|
|
320
|
+
layout.arrowEdge = placement.arrowEdge;
|
|
321
|
+
layout.top = clampTooltipCoordinate(placement.top, bounds.minTop, bounds.maxBottom - layout.height);
|
|
322
|
+
}
|
|
323
|
+
function getAboveBelowPlacementCandidates(layout, placedLayouts, bounds, preferredEdge) {
|
|
324
|
+
const oppositeEdge = preferredEdge === 'bottom' ? 'top' : 'bottom';
|
|
325
|
+
const candidates = [
|
|
326
|
+
getIdealAboveBelowPlacement(layout, preferredEdge),
|
|
327
|
+
getIdealAboveBelowPlacement(layout, oppositeEdge),
|
|
328
|
+
];
|
|
329
|
+
placedLayouts.forEach((placedLayout) => {
|
|
330
|
+
const maxAboveTargetTop = layout.targetY - TOOLTIP_OFFSET_PX - layout.height;
|
|
331
|
+
const minBelowTargetTop = layout.targetY + TOOLTIP_OFFSET_PX;
|
|
332
|
+
const abovePlacedTop = Math.min(placedLayout.top - layout.height - SPLIT_TOOLTIP_GAP_PX, maxAboveTargetTop);
|
|
333
|
+
const belowPlacedTop = Math.max(placedLayout.top + placedLayout.height + SPLIT_TOOLTIP_GAP_PX, minBelowTargetTop);
|
|
334
|
+
candidates.push({
|
|
335
|
+
top: abovePlacedTop,
|
|
336
|
+
arrowEdge: resolveAboveBelowArrowEdgeForTop(layout, abovePlacedTop),
|
|
337
|
+
});
|
|
338
|
+
candidates.push({
|
|
339
|
+
top: belowPlacedTop,
|
|
340
|
+
arrowEdge: resolveAboveBelowArrowEdgeForTop(layout, belowPlacedTop),
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
return candidates
|
|
344
|
+
.map((candidate) => ({
|
|
345
|
+
...candidate,
|
|
346
|
+
top: clampTooltipCoordinate(candidate.top, bounds.minTop, bounds.maxBottom - layout.height),
|
|
347
|
+
}))
|
|
348
|
+
.sort((a, b) => getAboveBelowPlacementCost(a, layout) -
|
|
349
|
+
getAboveBelowPlacementCost(b, layout));
|
|
350
|
+
}
|
|
351
|
+
function getIdealAboveBelowPlacement(layout, arrowEdge) {
|
|
352
|
+
return {
|
|
353
|
+
arrowEdge,
|
|
354
|
+
top: arrowEdge === 'bottom'
|
|
355
|
+
? layout.targetY - TOOLTIP_OFFSET_PX - layout.height
|
|
356
|
+
: layout.targetY + TOOLTIP_OFFSET_PX,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function getAboveBelowPlacementCost(candidate, layout) {
|
|
360
|
+
if (candidate.arrowEdge === 'bottom') {
|
|
361
|
+
return Math.abs(layout.targetY - (candidate.top + layout.height));
|
|
362
|
+
}
|
|
363
|
+
return Math.abs(candidate.top - layout.targetY);
|
|
364
|
+
}
|
|
365
|
+
function resolveAboveBelowArrowEdgeForTop(layout, top) {
|
|
366
|
+
const tooltipCenterY = top + layout.height / 2;
|
|
367
|
+
return tooltipCenterY <= layout.targetY ? 'bottom' : 'top';
|
|
368
|
+
}
|
|
369
|
+
function isSplitTooltipCandidateWithinBounds(candidate, tooltipHeight, bounds) {
|
|
370
|
+
return (candidate.top >= bounds.minTop &&
|
|
371
|
+
candidate.top + tooltipHeight <= bounds.maxBottom);
|
|
372
|
+
}
|
|
373
|
+
function isAboveBelowCandidateOnArrowSide(candidate, layout) {
|
|
374
|
+
if (candidate.arrowEdge === 'bottom') {
|
|
375
|
+
return (candidate.top + layout.height <= layout.targetY - TOOLTIP_OFFSET_PX);
|
|
376
|
+
}
|
|
377
|
+
return candidate.top >= layout.targetY + TOOLTIP_OFFSET_PX;
|
|
378
|
+
}
|
|
379
|
+
function doesSplitTooltipVerticalCandidateOverlap(candidate, tooltipHeight, placedLayouts) {
|
|
380
|
+
return placedLayouts.some((placedLayout) => {
|
|
381
|
+
return (candidate.top <
|
|
382
|
+
placedLayout.top + placedLayout.height + SPLIT_TOOLTIP_GAP_PX &&
|
|
383
|
+
candidate.top + tooltipHeight + SPLIT_TOOLTIP_GAP_PX >
|
|
384
|
+
placedLayout.top);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function clampTooltipCoordinate(value, minValue, maxValue) {
|
|
388
|
+
if (maxValue < minValue) {
|
|
389
|
+
return minValue;
|
|
390
|
+
}
|
|
391
|
+
return Math.max(minValue, Math.min(value, maxValue));
|
|
392
|
+
}
|
|
393
|
+
function hasFiniteNumbers(...values) {
|
|
394
|
+
return values.every((value) => Number.isFinite(value));
|
|
395
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Selection } from 'd3';
|
|
2
|
+
import type { Bar } from '../bar.js';
|
|
3
|
+
import type { Area } from '../area.js';
|
|
4
|
+
import type { Line } from '../line.js';
|
|
5
|
+
import type { Scatter } from '../scatter.js';
|
|
6
|
+
import type { TooltipTransitionConfig } from '../types.js';
|
|
7
|
+
export declare const TOOLTIP_OFFSET_PX = 12;
|
|
8
|
+
export declare const TOOLTIP_VIEWPORT_PADDING_PX = 10;
|
|
9
|
+
export declare const TOOLTIP_CONNECTOR_INSET_PX = 14;
|
|
10
|
+
export declare const TOOLTIP_CONNECTOR_PADDING_PX = 4;
|
|
11
|
+
export declare const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
|
|
12
|
+
export declare const TOOLTIP_BORDER_WIDTH_PX = 1;
|
|
13
|
+
export declare const TOOLTIP_BOX_ARROW_LENGTH_PX = 10;
|
|
14
|
+
export declare const TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX = 6;
|
|
15
|
+
export declare const TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX = 1;
|
|
16
|
+
export declare const TOOLTIP_CONNECTOR_Z_INDEX = 3;
|
|
17
|
+
export declare const TOOLTIP_ARROW_BORDER_Z_INDEX = 4;
|
|
18
|
+
export declare const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
|
|
19
|
+
export declare const TOOLTIP_BODY_Z_INDEX = 6;
|
|
20
|
+
export declare const TOOLTIP_TOTAL_BORDER_WIDTH_PX: number;
|
|
21
|
+
export declare const TOOLTIP_ROOT_Z_INDEX = 30;
|
|
22
|
+
export declare const SPLIT_TOOLTIP_GAP_PX = 8;
|
|
23
|
+
export declare const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
|
|
24
|
+
export declare const DEFAULT_TOOLTIP_TRANSITION: Required<TooltipTransitionConfig>;
|
|
25
|
+
export declare const TOOLTIP_HIDDEN_TRANSFORM = "translateY(2px)";
|
|
26
|
+
export declare const TOOLTIP_VISIBLE_TRANSFORM = "translateY(0)";
|
|
27
|
+
export type XYTooltipSeries = Line | Bar | Area | Scatter;
|
|
28
|
+
export type TooltipArrowEdge = 'left' | 'right' | 'top' | 'bottom';
|
|
29
|
+
export type TooltipDivSelection = Selection<HTMLDivElement, unknown, HTMLElement, undefined>;
|
|
30
|
+
export type TooltipAnchor = {
|
|
31
|
+
left: number;
|
|
32
|
+
right: number;
|
|
33
|
+
top: number;
|
|
34
|
+
bottom: number;
|
|
35
|
+
centerX: number;
|
|
36
|
+
centerY: number;
|
|
37
|
+
};
|
|
38
|
+
export type TooltipTarget = {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
};
|
|
42
|
+
export type TooltipBoxArrowPosition = {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
};
|
|
46
|
+
export type TooltipConnectorLayout = {
|
|
47
|
+
arrowEdge: TooltipArrowEdge;
|
|
48
|
+
left: number;
|
|
49
|
+
top: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
path: string;
|
|
53
|
+
arrowX: number;
|
|
54
|
+
arrowY: number;
|
|
55
|
+
};
|
|
56
|
+
export type SplitTooltipLayout = {
|
|
57
|
+
div: TooltipDivSelection;
|
|
58
|
+
anchor: TooltipAnchor;
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
left: number;
|
|
62
|
+
top: number;
|
|
63
|
+
arrowEdge: TooltipArrowEdge;
|
|
64
|
+
targetX: number;
|
|
65
|
+
targetY: number;
|
|
66
|
+
order: number;
|
|
67
|
+
};
|
|
68
|
+
export type SplitTooltipPlacementCandidate = {
|
|
69
|
+
top: number;
|
|
70
|
+
arrowEdge: TooltipArrowEdge;
|
|
71
|
+
};
|
|
72
|
+
export type SplitTooltipViewportBounds = {
|
|
73
|
+
minLeft: number;
|
|
74
|
+
maxRight: number;
|
|
75
|
+
minTop: number;
|
|
76
|
+
maxBottom: number;
|
|
77
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const TOOLTIP_OFFSET_PX = 12;
|
|
2
|
+
export const TOOLTIP_VIEWPORT_PADDING_PX = 10;
|
|
3
|
+
export const TOOLTIP_CONNECTOR_INSET_PX = 14;
|
|
4
|
+
export const TOOLTIP_CONNECTOR_PADDING_PX = 4;
|
|
5
|
+
export const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
|
|
6
|
+
export const TOOLTIP_BORDER_WIDTH_PX = 1;
|
|
7
|
+
export const TOOLTIP_BOX_ARROW_LENGTH_PX = 10;
|
|
8
|
+
export const TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX = 6;
|
|
9
|
+
export const TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX = 1;
|
|
10
|
+
export const TOOLTIP_CONNECTOR_Z_INDEX = 3;
|
|
11
|
+
export const TOOLTIP_ARROW_BORDER_Z_INDEX = 4;
|
|
12
|
+
export const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
|
|
13
|
+
export const TOOLTIP_BODY_Z_INDEX = 6;
|
|
14
|
+
export const TOOLTIP_TOTAL_BORDER_WIDTH_PX = TOOLTIP_BORDER_WIDTH_PX * 2;
|
|
15
|
+
export const TOOLTIP_ROOT_Z_INDEX = 30;
|
|
16
|
+
export const SPLIT_TOOLTIP_GAP_PX = 8;
|
|
17
|
+
export const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
|
|
18
|
+
export const DEFAULT_TOOLTIP_TRANSITION = {
|
|
19
|
+
show: false,
|
|
20
|
+
duration: 120,
|
|
21
|
+
easing: 'ease-out',
|
|
22
|
+
};
|
|
23
|
+
export const TOOLTIP_HIDDEN_TRANSFORM = 'translateY(2px)';
|
|
24
|
+
export const TOOLTIP_VISIBLE_TRANSFORM = 'translateY(0)';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { ChartTheme, DataItem, DataValue, D3Scale, ScaleType, TooltipBarAnchorPosition, TooltipMode, TooltipPosition } from '../types.js';
|
|
3
|
+
import type { PlotAreaBounds } from '../layout-manager.js';
|
|
4
|
+
import type { TooltipDom } from './dom.js';
|
|
5
|
+
import type { XYTooltipSeries } from './types.js';
|
|
6
|
+
type ResolveSeriesValue = (series: XYTooltipSeries, dataPoint: DataItem, index: number) => number;
|
|
7
|
+
export type XYTooltipAreaConfig = {
|
|
8
|
+
svg: Selection<SVGSVGElement, undefined, null, undefined>;
|
|
9
|
+
data: DataItem[];
|
|
10
|
+
series: XYTooltipSeries[];
|
|
11
|
+
xKey: string;
|
|
12
|
+
x: D3Scale;
|
|
13
|
+
y: D3Scale;
|
|
14
|
+
theme: ChartTheme;
|
|
15
|
+
plotArea: PlotAreaBounds;
|
|
16
|
+
parseValue: (value: unknown) => number;
|
|
17
|
+
isHorizontal: boolean;
|
|
18
|
+
categoryScaleType: ScaleType;
|
|
19
|
+
resolveSeriesValue?: ResolveSeriesValue;
|
|
20
|
+
mode: TooltipMode;
|
|
21
|
+
position: TooltipPosition;
|
|
22
|
+
barAnchorPosition: TooltipBarAnchorPosition;
|
|
23
|
+
formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
|
24
|
+
labelFormatter?: (label: string, data: DataItem) => string;
|
|
25
|
+
customFormatter?: (data: DataItem, series: {
|
|
26
|
+
dataKey: string;
|
|
27
|
+
stroke?: string;
|
|
28
|
+
fill?: string;
|
|
29
|
+
}[]) => string;
|
|
30
|
+
dom: TooltipDom;
|
|
31
|
+
};
|
|
32
|
+
export declare function attachXYTooltipArea(config: XYTooltipAreaConfig): (() => void) | null;
|
|
33
|
+
export {};
|