@internetstiftelsen/charts 0.18.0 → 0.18.2
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 +14 -1
- package/dist/theme.js +2 -0
- package/dist/tooltip/dom.d.ts +4 -1
- package/dist/tooltip/dom.js +8 -2
- package/dist/tooltip/geometry.js +204 -62
- package/dist/tooltip/types.d.ts +3 -2
- package/dist/tooltip/types.js +3 -2
- package/dist/tooltip/xy-interaction.d.ts +2 -0
- package/dist/tooltip/xy-interaction.js +12 -2
- package/dist/tooltip.d.ts +2 -0
- package/dist/tooltip.js +19 -1
- package/dist/types.d.ts +4 -0
- package/docs/components.md +14 -1
- package/docs/theming.md +4 -0
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
16
16
|
- **Optional Radial Animation** - Animate pie and donut segments on first render and `chart.update(...)` with `animate`
|
|
17
17
|
- **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
|
|
18
18
|
- **Stacking Control** - Bar and area stacking modes with optional reversed visual series order
|
|
19
|
-
- **Configurable Tooltips** - Shared, split, or single tooltips with connectors, transitions, color-coded series styling, and default max-width wrapping
|
|
19
|
+
- **Configurable Tooltips** - Shared, split, or single tooltips with connectors, transitions, color-coded series styling, optional series border and connector overrides, and default max-width wrapping
|
|
20
20
|
- **Axis Direction Control** - Use `scales.x.reverse` / `scales.y.reverse` to flip an axis when needed
|
|
21
21
|
- **Flexible Scales** - Band, linear, time, and logarithmic scales (bar value axes stay linear)
|
|
22
22
|
- **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
|
|
@@ -65,6 +65,19 @@ pnpm dev:docs
|
|
|
65
65
|
Runs the marketing landing page (`docs.html`) built on
|
|
66
66
|
`@internetstiftelsen/styleguide`.
|
|
67
67
|
|
|
68
|
+
```bash
|
|
69
|
+
pnpm storybook
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Runs Storybook on port 6006 with categorized chart examples for quickly
|
|
73
|
+
checking chart types and configuration combinations.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm build-storybook
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Builds the static Storybook output.
|
|
80
|
+
|
|
68
81
|
## Build Targets
|
|
69
82
|
|
|
70
83
|
```bash
|
package/dist/theme.js
CHANGED
|
@@ -107,6 +107,7 @@ export const defaultTheme = {
|
|
|
107
107
|
tooltip: {
|
|
108
108
|
background: '#ffffff',
|
|
109
109
|
border: '#dddddd',
|
|
110
|
+
seriesBorderColor: '#ffffff',
|
|
110
111
|
color: '#1f2a36',
|
|
111
112
|
fontFamily: SYSTEM_FONT,
|
|
112
113
|
fontSize: 12,
|
|
@@ -268,6 +269,7 @@ export const newspaperTheme = {
|
|
|
268
269
|
tooltip: {
|
|
269
270
|
background: '#111111',
|
|
270
271
|
border: '#111111',
|
|
272
|
+
seriesBorderColor: '#ffffff',
|
|
271
273
|
color: '#ffffff',
|
|
272
274
|
fontFamily: 'Georgia, "Times New Roman", Times, serif',
|
|
273
275
|
fontSize: 11,
|
package/dist/tooltip/dom.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ type TooltipDomConfig = {
|
|
|
6
6
|
maxWidth: number;
|
|
7
7
|
transition: Required<TooltipTransitionConfig>;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
type ResolvedTooltipStyle = ChartTheme['tooltip'] & {
|
|
10
|
+
connectorColor: string;
|
|
11
|
+
};
|
|
12
|
+
export type TooltipStyleOverrides = Partial<Pick<ResolvedTooltipStyle, 'background' | 'border' | 'color' | 'connectorColor'>>;
|
|
10
13
|
export declare class TooltipDom {
|
|
11
14
|
private readonly id;
|
|
12
15
|
private readonly splitTooltipOwner;
|
package/dist/tooltip/dom.js
CHANGED
|
@@ -214,15 +214,21 @@ export class TooltipDom {
|
|
|
214
214
|
this.writeTooltipStyles(tooltip, tooltipStyle);
|
|
215
215
|
}
|
|
216
216
|
resolveTooltipStyle(theme, styleOverrides) {
|
|
217
|
+
const border = styleOverrides?.border ?? theme.tooltip.border;
|
|
217
218
|
return {
|
|
218
219
|
...theme.tooltip,
|
|
219
220
|
...styleOverrides,
|
|
221
|
+
border,
|
|
222
|
+
connectorColor: styleOverrides?.connectorColor ??
|
|
223
|
+
styleOverrides?.border ??
|
|
224
|
+
theme.tooltip.border,
|
|
220
225
|
};
|
|
221
226
|
}
|
|
222
227
|
getTooltipStyleKey(tooltipStyle) {
|
|
223
228
|
return [
|
|
224
229
|
tooltipStyle.background,
|
|
225
230
|
tooltipStyle.border,
|
|
231
|
+
tooltipStyle.connectorColor,
|
|
226
232
|
tooltipStyle.color,
|
|
227
233
|
tooltipStyle.fontFamily,
|
|
228
234
|
tooltipStyle.fontSize,
|
|
@@ -365,7 +371,7 @@ export class TooltipDom {
|
|
|
365
371
|
}
|
|
366
372
|
appendTooltipConnector(tooltip, connectorLayout) {
|
|
367
373
|
const tooltipStyle = this.getTooltipStyle(tooltip);
|
|
368
|
-
const
|
|
374
|
+
const connectorColor = tooltipStyle?.connectorColor ?? '#dddddd';
|
|
369
375
|
const connector = tooltip
|
|
370
376
|
.append('svg')
|
|
371
377
|
.attr('data-chart-tooltip-connector', 'true')
|
|
@@ -385,7 +391,7 @@ export class TooltipDom {
|
|
|
385
391
|
.attr('data-chart-tooltip-connector-path', 'true')
|
|
386
392
|
.attr('d', connectorLayout.path)
|
|
387
393
|
.attr('fill', 'none')
|
|
388
|
-
.attr('stroke',
|
|
394
|
+
.attr('stroke', connectorColor)
|
|
389
395
|
.attr('stroke-width', 1.25)
|
|
390
396
|
.attr('stroke-linecap', 'round')
|
|
391
397
|
.attr('stroke-linejoin', 'round');
|
package/dist/tooltip/geometry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
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_MIN_PATH_LENGTH_PX, TOOLTIP_CONNECTOR_PADDING_PX, TOOLTIP_OFFSET_PX, TOOLTIP_TOTAL_BORDER_WIDTH_PX, TOOLTIP_VIEWPORT_PADDING_PX, } from './types.js';
|
|
2
2
|
const MAX_EXHAUSTIVE_HORIZONTAL_ROW_LAYOUTS = 10;
|
|
3
3
|
const HORIZONTAL_ROW_ORDER_FLIP_PENALTY_PX = 128;
|
|
4
|
+
const AUTO_BAR_TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX = 8;
|
|
4
5
|
export function getSplitTooltipViewportBounds() {
|
|
5
6
|
const visualViewport = window.visualViewport;
|
|
6
7
|
const viewportLeft = window.scrollX + (visualViewport?.offsetLeft ?? 0);
|
|
@@ -98,13 +99,16 @@ export function getAnchoredTooltipPosition(anchor, target, tooltipWidth, tooltip
|
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
102
|
+
const boxArrowPosition = resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
103
|
+
const arrowTip = resolveTooltipArrowTip(arrowEdge, boxArrowPosition.x, boxArrowPosition.y, TOOLTIP_BOX_ARROW_LENGTH_PX);
|
|
104
|
+
const arrowTipX = tooltipLeft + arrowTip.x;
|
|
105
|
+
const arrowTipY = tooltipTop + arrowTip.y;
|
|
106
|
+
const connectorTarget = resolveTooltipConnectorTarget(arrowEdge, targetX, targetY, arrowTipX, arrowTipY, anchor);
|
|
107
|
+
const localTargetX = connectorTarget.x - tooltipLeft;
|
|
108
|
+
const localTargetY = connectorTarget.y - tooltipTop;
|
|
103
109
|
if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
|
|
104
110
|
return null;
|
|
105
111
|
}
|
|
106
|
-
const boxArrowPosition = resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
107
|
-
const arrowTip = resolveTooltipArrowTip(arrowEdge, boxArrowPosition.x, boxArrowPosition.y, TOOLTIP_BOX_ARROW_LENGTH_PX);
|
|
108
112
|
const minX = Math.min(arrowTip.x, localTargetX) - TOOLTIP_CONNECTOR_PADDING_PX;
|
|
109
113
|
const maxX = Math.max(arrowTip.x, localTargetX) + TOOLTIP_CONNECTOR_PADDING_PX;
|
|
110
114
|
const minY = Math.min(arrowTip.y, localTargetY) - TOOLTIP_CONNECTOR_PADDING_PX;
|
|
@@ -117,9 +121,7 @@ export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop
|
|
|
117
121
|
const startY = arrowTip.y - minY;
|
|
118
122
|
const endX = localTargetX - minX;
|
|
119
123
|
const endY = localTargetY - minY;
|
|
120
|
-
const
|
|
121
|
-
const arrowTipY = tooltipTop + arrowTip.y;
|
|
122
|
-
const connectorPath = resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor);
|
|
124
|
+
const connectorPath = resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor, connectorTarget.preferDirectPath);
|
|
123
125
|
if (!hasFiniteNumbers(width, height, boxX, boxY, startX, startY, endX, endY)) {
|
|
124
126
|
return null;
|
|
125
127
|
}
|
|
@@ -134,6 +136,43 @@ export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop
|
|
|
134
136
|
arrowY: boxArrowPosition.y,
|
|
135
137
|
};
|
|
136
138
|
}
|
|
139
|
+
function resolveTooltipConnectorTarget(arrowEdge, targetX, targetY, arrowTipX, arrowTipY, anchor) {
|
|
140
|
+
if (isPointTooltipAnchor(anchor)) {
|
|
141
|
+
return { x: targetX, y: targetY, preferDirectPath: false };
|
|
142
|
+
}
|
|
143
|
+
if (arrowEdge === 'left') {
|
|
144
|
+
return {
|
|
145
|
+
x: anchor.right,
|
|
146
|
+
y: clampTooltipCoordinate(arrowTipY, anchor.top, anchor.bottom),
|
|
147
|
+
preferDirectPath: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (arrowEdge === 'right') {
|
|
151
|
+
return {
|
|
152
|
+
x: anchor.left,
|
|
153
|
+
y: clampTooltipCoordinate(arrowTipY, anchor.top, anchor.bottom),
|
|
154
|
+
preferDirectPath: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (arrowEdge === 'top') {
|
|
158
|
+
return {
|
|
159
|
+
x: clampTooltipCoordinate(arrowTipX, anchor.left, anchor.right),
|
|
160
|
+
y: anchor.bottom,
|
|
161
|
+
preferDirectPath: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
x: clampTooltipCoordinate(arrowTipX, anchor.left, anchor.right),
|
|
166
|
+
y: anchor.top,
|
|
167
|
+
preferDirectPath: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function isPointTooltipAnchor(anchor) {
|
|
171
|
+
return (Math.abs(anchor.left - anchor.right) <=
|
|
172
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
173
|
+
Math.abs(anchor.top - anchor.bottom) <=
|
|
174
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
175
|
+
}
|
|
137
176
|
export function resolveTooltipArrowPosition(arrowEdge, boxX, boxY, length, halfHeight) {
|
|
138
177
|
switch (arrowEdge) {
|
|
139
178
|
case 'left':
|
|
@@ -175,7 +214,7 @@ export function resolveSplitTooltipPositions(layouts, position, bounds = getSpli
|
|
|
175
214
|
return;
|
|
176
215
|
}
|
|
177
216
|
const arrowEdge = resolveSidePlacementArrowEdge(getCombinedSplitTooltipAnchor(layouts), Math.max(...layouts.map((layout) => layout.width)), bounds);
|
|
178
|
-
const orderedLayouts =
|
|
217
|
+
const orderedLayouts = getSideSplitTooltipLayouts(layouts);
|
|
179
218
|
positionSideSplitTooltipStack(orderedLayouts, arrowEdge, bounds);
|
|
180
219
|
}
|
|
181
220
|
function resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
@@ -210,17 +249,26 @@ function getTooltipConnectorOffset(start, size, target) {
|
|
|
210
249
|
const preferredOffset = target - start;
|
|
211
250
|
return Math.max(minOffset, Math.min(preferredOffset, maxOffset));
|
|
212
251
|
}
|
|
213
|
-
function resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
|
|
252
|
+
function resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor, preferDirectPath) {
|
|
214
253
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
215
|
-
|
|
254
|
+
const minPathLength = preferDirectPath
|
|
255
|
+
? AUTO_BAR_TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX
|
|
256
|
+
: TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX;
|
|
257
|
+
if (isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor, minPathLength)) {
|
|
216
258
|
return '';
|
|
217
259
|
}
|
|
260
|
+
if (preferDirectPath) {
|
|
261
|
+
return `M ${startX},${startY} L ${endX},${endY}`;
|
|
262
|
+
}
|
|
218
263
|
const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
219
264
|
return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
|
|
220
265
|
}
|
|
221
266
|
if (isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor)) {
|
|
222
267
|
return '';
|
|
223
268
|
}
|
|
269
|
+
if (preferDirectPath) {
|
|
270
|
+
return `M ${startX},${startY} L ${endX},${endY}`;
|
|
271
|
+
}
|
|
224
272
|
const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
225
273
|
return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
|
|
226
274
|
}
|
|
@@ -232,6 +280,13 @@ function isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor) {
|
|
|
232
280
|
return (arrowTipY >= anchor.top - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
233
281
|
arrowTipY <= anchor.bottom + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
234
282
|
}
|
|
283
|
+
function isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor, minPathLength = TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX) {
|
|
284
|
+
if (isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return (arrowTipY >= anchor.top - minPathLength &&
|
|
288
|
+
arrowTipY <= anchor.bottom + minPathLength);
|
|
289
|
+
}
|
|
235
290
|
function resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
|
|
236
291
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
237
292
|
return {
|
|
@@ -278,6 +333,15 @@ function getCombinedSplitTooltipTarget(layouts) {
|
|
|
278
333
|
y: (minY + maxY) / 2,
|
|
279
334
|
};
|
|
280
335
|
}
|
|
336
|
+
function getSideSplitTooltipLayouts(layouts) {
|
|
337
|
+
if (layouts.every((layout) => layout.targetMode === 'auto')) {
|
|
338
|
+
return [...layouts].sort((a, b) => a.anchor.top - b.anchor.top ||
|
|
339
|
+
a.anchor.bottom - b.anchor.bottom ||
|
|
340
|
+
a.anchor.left - b.anchor.left ||
|
|
341
|
+
a.order - b.order);
|
|
342
|
+
}
|
|
343
|
+
return getVerticallyOrderedSplitTooltipLayouts(layouts);
|
|
344
|
+
}
|
|
281
345
|
function resolveHorizontalSideSplitTooltipPositions(layouts, bounds) {
|
|
282
346
|
const rowPlacement = getBestHorizontalSideRowPlacement(getHorizontallyOrderedSplitTooltipLayouts(layouts), bounds);
|
|
283
347
|
rowPlacement.placements.forEach(({ layout, left, top, arrowEdge }) => {
|
|
@@ -424,6 +488,7 @@ function getHorizontalAboveBelowRowPlacement(layouts, assignment, preferredEdges
|
|
|
424
488
|
const rowLayouts = layouts.filter((_, index) => assignment[index] === arrowEdge);
|
|
425
489
|
return packHorizontalAboveBelowRow(rowLayouts, arrowEdge, bounds);
|
|
426
490
|
});
|
|
491
|
+
separateOverlappingHorizontalTooltipRows(placements, bounds);
|
|
427
492
|
const preferredEdgeByLayout = new Map(layouts.map((layout, index) => [layout, preferredEdges[index]]));
|
|
428
493
|
const score = placements.reduce((sum, placement) => {
|
|
429
494
|
const preferredEdge = preferredEdgeByLayout.get(placement.layout) ??
|
|
@@ -493,6 +558,59 @@ function packHorizontalTooltipRow(placements, bounds) {
|
|
|
493
558
|
}
|
|
494
559
|
return placements;
|
|
495
560
|
}
|
|
561
|
+
function separateOverlappingHorizontalTooltipRows(placements, bounds) {
|
|
562
|
+
if (placements.length < 2) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const orderedPlacements = [...placements].sort((a, b) => a.top - b.top ||
|
|
566
|
+
a.left - b.left ||
|
|
567
|
+
a.layout.targetX - b.layout.targetX ||
|
|
568
|
+
a.layout.order - b.layout.order);
|
|
569
|
+
for (let index = 0; index < orderedPlacements.length; index++) {
|
|
570
|
+
const placement = orderedPlacements[index];
|
|
571
|
+
for (let previousIndex = 0; previousIndex < index; previousIndex++) {
|
|
572
|
+
const previousPlacement = orderedPlacements[previousIndex];
|
|
573
|
+
if (!doHorizontalTooltipPlacementsOverlap(placement, previousPlacement)) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
placement.top = Math.max(placement.top, previousPlacement.top +
|
|
577
|
+
previousPlacement.layout.height +
|
|
578
|
+
SPLIT_TOOLTIP_GAP_PX);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const overflow = Math.max(...orderedPlacements.map((placement) => placement.top + placement.layout.height)) - bounds.maxBottom;
|
|
582
|
+
if (overflow > 0) {
|
|
583
|
+
orderedPlacements.forEach((placement) => {
|
|
584
|
+
placement.top -= overflow;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
for (let index = orderedPlacements.length - 2; index >= 0; index--) {
|
|
588
|
+
const placement = orderedPlacements[index];
|
|
589
|
+
for (let nextIndex = orderedPlacements.length - 1; nextIndex > index; nextIndex--) {
|
|
590
|
+
const nextPlacement = orderedPlacements[nextIndex];
|
|
591
|
+
if (!doHorizontalTooltipPlacementsOverlap(placement, nextPlacement)) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
placement.top = Math.min(placement.top, nextPlacement.top -
|
|
595
|
+
placement.layout.height -
|
|
596
|
+
SPLIT_TOOLTIP_GAP_PX);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const underflow = bounds.minTop -
|
|
600
|
+
Math.min(...orderedPlacements.map((placement) => placement.top));
|
|
601
|
+
if (underflow <= 0) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
orderedPlacements.forEach((placement) => {
|
|
605
|
+
placement.top += underflow;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function doHorizontalTooltipPlacementsOverlap(first, second) {
|
|
609
|
+
return (first.left < second.left + second.layout.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
610
|
+
first.left + first.layout.width + SPLIT_TOOLTIP_GAP_PX > second.left &&
|
|
611
|
+
first.top < second.top + second.layout.height + SPLIT_TOOLTIP_GAP_PX &&
|
|
612
|
+
first.top + first.layout.height + SPLIT_TOOLTIP_GAP_PX > second.top);
|
|
613
|
+
}
|
|
496
614
|
function getHorizontalAboveBelowIdealLeft(layout) {
|
|
497
615
|
const centeredLeft = layout.targetX - layout.width / 2;
|
|
498
616
|
if (layout.targetMode === 'auto') {
|
|
@@ -513,8 +631,13 @@ function getHorizontalAboveBelowPlacementCost(candidate, layout, preferredEdge,
|
|
|
513
631
|
? 0
|
|
514
632
|
: layout.height * 4;
|
|
515
633
|
const xDistance = Math.abs(layout.targetX - (candidate.left + layout.width / 2));
|
|
634
|
+
const yDistance = Math.abs(candidate.top - getHorizontalAboveBelowTop(layout, candidate.arrowEdge));
|
|
516
635
|
const overflowPenalty = getSplitTooltipBoxPlacementOverflow(candidate, layout, bounds) * 20;
|
|
517
|
-
return connectorPenalty +
|
|
636
|
+
return (connectorPenalty +
|
|
637
|
+
edgePenalty +
|
|
638
|
+
xDistance +
|
|
639
|
+
yDistance * 0.5 +
|
|
640
|
+
overflowPenalty);
|
|
518
641
|
}
|
|
519
642
|
function getPreferredAndOppositeEdges(preferredEdge) {
|
|
520
643
|
if (preferredEdge === 'bottom') {
|
|
@@ -544,28 +667,14 @@ function getSplitTooltipBoxPlacementOverflow(candidate, layout, bounds) {
|
|
|
544
667
|
Math.max(0, candidate.top + layout.height - bounds.maxBottom));
|
|
545
668
|
}
|
|
546
669
|
function positionSideSplitTooltipStack(layouts, arrowEdge, bounds) {
|
|
547
|
-
const placementOrder = getSidePlacementOrder(layouts);
|
|
548
670
|
const placedLayouts = [];
|
|
549
|
-
|
|
671
|
+
layouts.forEach((layout) => {
|
|
550
672
|
layout.arrowEdge = arrowEdge;
|
|
551
673
|
layout.left = getSideTooltipLeft(layout, arrowEdge, bounds);
|
|
552
674
|
positionSideSplitTooltip(layout, placedLayouts, layouts, bounds);
|
|
553
675
|
placedLayouts.push(layout);
|
|
554
676
|
});
|
|
555
677
|
}
|
|
556
|
-
function getSidePlacementOrder(layouts) {
|
|
557
|
-
if (!layouts.every((layout) => layout.targetMode === 'auto')) {
|
|
558
|
-
return layouts;
|
|
559
|
-
}
|
|
560
|
-
return [...layouts].sort((a, b) => {
|
|
561
|
-
return (getTooltipAnchorVerticalSpan(a) - getTooltipAnchorVerticalSpan(b) ||
|
|
562
|
-
a.targetY - b.targetY ||
|
|
563
|
-
a.order - b.order);
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
function getTooltipAnchorVerticalSpan(layout) {
|
|
567
|
-
return layout.anchor.bottom - layout.anchor.top;
|
|
568
|
-
}
|
|
569
678
|
function getSideTooltipLeft(layout, arrowEdge, bounds) {
|
|
570
679
|
const preferredLeft = arrowEdge === 'left'
|
|
571
680
|
? layout.anchor.right + TOOLTIP_OFFSET_PX
|
|
@@ -602,6 +711,15 @@ function getSidePlacementCandidates(layout, placedLayouts, allLayouts, bounds) {
|
|
|
602
711
|
];
|
|
603
712
|
if (layout.targetMode === 'auto') {
|
|
604
713
|
const connectorlessRange = getSideConnectorlessTopRange(layout);
|
|
714
|
+
const exactConnectorlessRange = getExactSideConnectorlessTopRange(layout);
|
|
715
|
+
candidates.push({
|
|
716
|
+
top: exactConnectorlessRange.min,
|
|
717
|
+
arrowEdge: layout.arrowEdge,
|
|
718
|
+
});
|
|
719
|
+
candidates.push({
|
|
720
|
+
top: exactConnectorlessRange.max,
|
|
721
|
+
arrowEdge: layout.arrowEdge,
|
|
722
|
+
});
|
|
605
723
|
candidates.push({
|
|
606
724
|
top: connectorlessRange.min,
|
|
607
725
|
arrowEdge: layout.arrowEdge,
|
|
@@ -630,55 +748,77 @@ function getSidePlacementCandidates(layout, placedLayouts, allLayouts, bounds) {
|
|
|
630
748
|
getSidePlacementCost(b, layout, allLayouts));
|
|
631
749
|
}
|
|
632
750
|
function getSidePlacementCost(candidate, layout, allLayouts) {
|
|
633
|
-
const
|
|
634
|
-
? 0
|
|
635
|
-
: layout.height * 4;
|
|
751
|
+
const connectorless = isSidePlacementConnectorless(candidate, layout);
|
|
636
752
|
const centerDistance = Math.abs(layout.targetY - (candidate.top + layout.height / 2));
|
|
637
|
-
|
|
638
|
-
|
|
753
|
+
const arrowDistance = Math.abs(layout.targetY - getSidePlacementArrowTipY(candidate.top, layout));
|
|
754
|
+
if (layout.targetMode === 'auto' && connectorless) {
|
|
755
|
+
const preferredTop = layout.targetY - getTooltipConnectorMaxOffset(layout.height);
|
|
756
|
+
const preferredTopDistance = Math.abs(candidate.top - preferredTop);
|
|
757
|
+
return preferredTopDistance + arrowDistance * 0.1;
|
|
758
|
+
}
|
|
759
|
+
if (connectorless) {
|
|
760
|
+
const spreadDirection = getFixedPointSideSpreadDirection(layout, allLayouts);
|
|
639
761
|
if (spreadDirection !== 0) {
|
|
640
|
-
const
|
|
641
|
-
const preferredTop = spreadDirection > 0
|
|
642
|
-
? connectorlessRange.max
|
|
643
|
-
: connectorlessRange.min;
|
|
762
|
+
const preferredTop = getFixedPointSideSpreadPreferredTop(layout, spreadDirection);
|
|
644
763
|
return (Math.abs(candidate.top - preferredTop) + centerDistance * 0.05);
|
|
645
764
|
}
|
|
765
|
+
return centerDistance;
|
|
646
766
|
}
|
|
647
|
-
return
|
|
767
|
+
return layout.height * 4 + arrowDistance + centerDistance * 0.2;
|
|
648
768
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
const closestBelow = getClosestAutoSideNeighbor(layout, allLayouts, 'below');
|
|
652
|
-
if (!closestAbove && closestBelow) {
|
|
653
|
-
return -1;
|
|
654
|
-
}
|
|
655
|
-
if (closestAbove && !closestBelow) {
|
|
656
|
-
return 1;
|
|
657
|
-
}
|
|
658
|
-
if (!closestAbove || !closestBelow) {
|
|
769
|
+
function getFixedPointSideSpreadDirection(layout, allLayouts) {
|
|
770
|
+
if (!isFixedPointSideTooltipLayout(layout)) {
|
|
659
771
|
return 0;
|
|
660
772
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
773
|
+
const nearbyLayouts = allLayouts
|
|
774
|
+
.filter((otherLayout) => isFixedPointSideTooltipLayout(otherLayout) &&
|
|
775
|
+
areFixedPointSideTargetsNear(layout, otherLayout))
|
|
776
|
+
.sort((a, b) => a.targetY - b.targetY ||
|
|
777
|
+
a.targetX - b.targetX ||
|
|
778
|
+
a.order - b.order);
|
|
779
|
+
if (nearbyLayouts.length < 2) {
|
|
780
|
+
return 0;
|
|
665
781
|
}
|
|
666
|
-
|
|
782
|
+
const layoutIndex = nearbyLayouts.indexOf(layout);
|
|
783
|
+
const clusterCenterIndex = (nearbyLayouts.length - 1) / 2;
|
|
784
|
+
if (layoutIndex < clusterCenterIndex) {
|
|
667
785
|
return -1;
|
|
668
786
|
}
|
|
787
|
+
if (layoutIndex > clusterCenterIndex) {
|
|
788
|
+
return 1;
|
|
789
|
+
}
|
|
669
790
|
return 0;
|
|
670
791
|
}
|
|
671
|
-
function
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
792
|
+
function isFixedPointSideTooltipLayout(layout) {
|
|
793
|
+
return (layout.targetMode === 'fixed' &&
|
|
794
|
+
Math.abs(layout.anchor.left - layout.anchor.right) <=
|
|
795
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
796
|
+
Math.abs(layout.anchor.top - layout.anchor.bottom) <=
|
|
797
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
798
|
+
}
|
|
799
|
+
function areFixedPointSideTargetsNear(layout, otherLayout) {
|
|
800
|
+
const centeredTooltipsOverlapDistance = (layout.height + otherLayout.height) / 2 + SPLIT_TOOLTIP_GAP_PX;
|
|
801
|
+
return (Math.abs(layout.targetY - otherLayout.targetY) <
|
|
802
|
+
centeredTooltipsOverlapDistance);
|
|
803
|
+
}
|
|
804
|
+
function getFixedPointSideSpreadPreferredTop(layout, direction) {
|
|
805
|
+
if (direction < 0) {
|
|
806
|
+
return layout.targetY - getTooltipConnectorMaxOffset(layout.height);
|
|
807
|
+
}
|
|
808
|
+
return layout.targetY - TOOLTIP_CONNECTOR_INSET_PX;
|
|
680
809
|
}
|
|
681
810
|
function getSideConnectorlessTopRange(layout) {
|
|
811
|
+
const nearPathPadding = layout.targetMode === 'auto'
|
|
812
|
+
? AUTO_BAR_TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX
|
|
813
|
+
: 0;
|
|
814
|
+
return {
|
|
815
|
+
min: layout.anchor.top -
|
|
816
|
+
getTooltipConnectorMaxOffset(layout.height) -
|
|
817
|
+
nearPathPadding,
|
|
818
|
+
max: layout.anchor.bottom - TOOLTIP_CONNECTOR_INSET_PX + nearPathPadding,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function getExactSideConnectorlessTopRange(layout) {
|
|
682
822
|
return {
|
|
683
823
|
min: layout.anchor.top - getTooltipConnectorMaxOffset(layout.height),
|
|
684
824
|
max: layout.anchor.bottom - TOOLTIP_CONNECTOR_INSET_PX,
|
|
@@ -686,7 +826,9 @@ function getSideConnectorlessTopRange(layout) {
|
|
|
686
826
|
}
|
|
687
827
|
function isSidePlacementConnectorless(candidate, layout) {
|
|
688
828
|
if (layout.targetMode === 'auto') {
|
|
689
|
-
|
|
829
|
+
const connectorlessRange = getSideConnectorlessTopRange(layout);
|
|
830
|
+
return (candidate.top >= connectorlessRange.min &&
|
|
831
|
+
candidate.top <= connectorlessRange.max);
|
|
690
832
|
}
|
|
691
833
|
const arrowTipY = getSidePlacementArrowTipY(candidate.top, layout);
|
|
692
834
|
return (arrowTipY >=
|
package/dist/tooltip/types.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type { Scatter } from '../scatter.js';
|
|
|
6
6
|
import type { TooltipTransitionConfig } from '../types.js';
|
|
7
7
|
export declare const TOOLTIP_OFFSET_PX = 12;
|
|
8
8
|
export declare const TOOLTIP_VIEWPORT_PADDING_PX = 10;
|
|
9
|
-
export declare const TOOLTIP_CONNECTOR_INSET_PX =
|
|
9
|
+
export declare const TOOLTIP_CONNECTOR_INSET_PX = 10;
|
|
10
|
+
export declare const TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX = 16;
|
|
10
11
|
export declare const TOOLTIP_CONNECTOR_PADDING_PX = 4;
|
|
11
12
|
export declare const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
|
|
12
13
|
export declare const TOOLTIP_BORDER_WIDTH_PX = 1;
|
|
@@ -19,7 +20,7 @@ export declare const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
|
|
|
19
20
|
export declare const TOOLTIP_BODY_Z_INDEX = 6;
|
|
20
21
|
export declare const TOOLTIP_TOTAL_BORDER_WIDTH_PX: number;
|
|
21
22
|
export declare const TOOLTIP_ROOT_Z_INDEX = 30;
|
|
22
|
-
export declare const SPLIT_TOOLTIP_GAP_PX =
|
|
23
|
+
export declare const SPLIT_TOOLTIP_GAP_PX = 4;
|
|
23
24
|
export declare const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
|
|
24
25
|
export declare const DEFAULT_TOOLTIP_TRANSITION: Required<TooltipTransitionConfig>;
|
|
25
26
|
export declare const TOOLTIP_HIDDEN_TRANSFORM = "translateY(2px)";
|
package/dist/tooltip/types.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const TOOLTIP_OFFSET_PX = 12;
|
|
2
2
|
export const TOOLTIP_VIEWPORT_PADDING_PX = 10;
|
|
3
|
-
export const TOOLTIP_CONNECTOR_INSET_PX =
|
|
3
|
+
export const TOOLTIP_CONNECTOR_INSET_PX = 10;
|
|
4
|
+
export const TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX = 16;
|
|
4
5
|
export const TOOLTIP_CONNECTOR_PADDING_PX = 4;
|
|
5
6
|
export const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
|
|
6
7
|
export const TOOLTIP_BORDER_WIDTH_PX = 1;
|
|
@@ -13,7 +14,7 @@ export const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
|
|
|
13
14
|
export const TOOLTIP_BODY_Z_INDEX = 6;
|
|
14
15
|
export const TOOLTIP_TOTAL_BORDER_WIDTH_PX = TOOLTIP_BORDER_WIDTH_PX * 2;
|
|
15
16
|
export const TOOLTIP_ROOT_Z_INDEX = 30;
|
|
16
|
-
export const SPLIT_TOOLTIP_GAP_PX =
|
|
17
|
+
export const SPLIT_TOOLTIP_GAP_PX = 4;
|
|
17
18
|
export const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
|
|
18
19
|
export const DEFAULT_TOOLTIP_TRANSITION = {
|
|
19
20
|
show: false,
|
|
@@ -21,6 +21,8 @@ export type XYTooltipAreaConfig = {
|
|
|
21
21
|
position: TooltipPosition;
|
|
22
22
|
barAnchorPosition: TooltipBarAnchorPosition;
|
|
23
23
|
colorMode: TooltipColorMode;
|
|
24
|
+
seriesBorderColor?: string;
|
|
25
|
+
seriesConnectorColor?: string;
|
|
24
26
|
formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
|
25
27
|
labelFormatter?: (label: string, data: DataItem) => string;
|
|
26
28
|
customFormatter?: (data: DataItem, series: {
|
|
@@ -3,7 +3,7 @@ import { getSeriesColor } from '../types.js';
|
|
|
3
3
|
import { getContrastTextColor, sanitizeForCSS } from '../utils.js';
|
|
4
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, colorMode, formatter, labelFormatter, customFormatter, dom, } = config;
|
|
6
|
+
const { svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal, categoryScaleType, mode, position, barAnchorPosition, colorMode, seriesBorderColor, seriesConnectorColor, formatter, labelFormatter, customFormatter, dom, } = config;
|
|
7
7
|
const tooltip = dom.getRootTooltip();
|
|
8
8
|
if (!tooltip || data.length === 0) {
|
|
9
9
|
return null;
|
|
@@ -63,7 +63,12 @@ export function attachXYTooltipArea(config) {
|
|
|
63
63
|
: getSeriesColor(currentSeries);
|
|
64
64
|
return {
|
|
65
65
|
background: seriesColor,
|
|
66
|
-
border:
|
|
66
|
+
border: seriesBorderColor ??
|
|
67
|
+
theme.tooltip.seriesBorderColor ??
|
|
68
|
+
seriesColor,
|
|
69
|
+
connectorColor: seriesConnectorColor ??
|
|
70
|
+
theme.tooltip.seriesConnectorColor ??
|
|
71
|
+
seriesColor,
|
|
67
72
|
color: getContrastTextColor(seriesColor),
|
|
68
73
|
};
|
|
69
74
|
};
|
|
@@ -257,6 +262,11 @@ export function attachXYTooltipArea(config) {
|
|
|
257
262
|
return;
|
|
258
263
|
}
|
|
259
264
|
const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
|
|
265
|
+
if (currentSeries.type === 'bar' &&
|
|
266
|
+
!isHorizontal &&
|
|
267
|
+
resolvedBarAnchorPosition === 'auto') {
|
|
268
|
+
target.y = visibleAnchor.top;
|
|
269
|
+
}
|
|
260
270
|
const targetMode = currentSeries.type === 'bar' &&
|
|
261
271
|
resolvedBarAnchorPosition === 'auto'
|
|
262
272
|
? 'auto'
|
package/dist/tooltip.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
11
11
|
readonly position: TooltipPosition;
|
|
12
12
|
readonly barAnchorPosition: TooltipBarAnchorPosition;
|
|
13
13
|
readonly colorMode: TooltipColorMode;
|
|
14
|
+
readonly seriesBorderColor?: string;
|
|
15
|
+
readonly seriesConnectorColor?: string;
|
|
14
16
|
readonly maxWidth: number;
|
|
15
17
|
readonly transition: Required<TooltipTransitionConfig>;
|
|
16
18
|
readonly formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
package/dist/tooltip.js
CHANGED
|
@@ -40,6 +40,18 @@ export class Tooltip {
|
|
|
40
40
|
writable: true,
|
|
41
41
|
value: void 0
|
|
42
42
|
});
|
|
43
|
+
Object.defineProperty(this, "seriesBorderColor", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: void 0
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(this, "seriesConnectorColor", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: void 0
|
|
54
|
+
});
|
|
43
55
|
Object.defineProperty(this, "maxWidth", {
|
|
44
56
|
enumerable: true,
|
|
45
57
|
configurable: true,
|
|
@@ -88,13 +100,15 @@ export class Tooltip {
|
|
|
88
100
|
writable: true,
|
|
89
101
|
value: null
|
|
90
102
|
});
|
|
91
|
-
const { mode = 'split', position = 'auto', barAnchorPosition = 'auto', colorMode = 'theme', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
|
|
103
|
+
const { mode = 'split', position = 'auto', barAnchorPosition = 'auto', colorMode = 'theme', seriesBorderColor, seriesConnectorColor, maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
|
|
92
104
|
const tooltipId = Tooltip.nextTooltipId++;
|
|
93
105
|
this.id = `iisChartTooltip-${tooltipId}`;
|
|
94
106
|
this.mode = mode;
|
|
95
107
|
this.position = position;
|
|
96
108
|
this.barAnchorPosition = barAnchorPosition;
|
|
97
109
|
this.colorMode = colorMode;
|
|
110
|
+
this.seriesBorderColor = seriesBorderColor;
|
|
111
|
+
this.seriesConnectorColor = seriesConnectorColor;
|
|
98
112
|
this.maxWidth =
|
|
99
113
|
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
100
114
|
? maxWidth
|
|
@@ -120,6 +134,8 @@ export class Tooltip {
|
|
|
120
134
|
position: this.position,
|
|
121
135
|
barAnchorPosition: this.barAnchorPosition,
|
|
122
136
|
colorMode: this.colorMode,
|
|
137
|
+
seriesBorderColor: this.seriesBorderColor,
|
|
138
|
+
seriesConnectorColor: this.seriesConnectorColor,
|
|
123
139
|
maxWidth: this.maxWidth,
|
|
124
140
|
transition: this.transition,
|
|
125
141
|
formatter: this.formatter,
|
|
@@ -162,6 +178,8 @@ export class Tooltip {
|
|
|
162
178
|
position: this.position,
|
|
163
179
|
barAnchorPosition: this.barAnchorPosition,
|
|
164
180
|
colorMode: this.colorMode,
|
|
181
|
+
seriesBorderColor: this.seriesBorderColor,
|
|
182
|
+
seriesConnectorColor: this.seriesConnectorColor,
|
|
165
183
|
formatter: this.formatter,
|
|
166
184
|
labelFormatter: this.labelFormatter,
|
|
167
185
|
customFormatter: this.customFormatter,
|
package/dist/types.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export type ChartTheme = {
|
|
|
74
74
|
tooltip: {
|
|
75
75
|
background: string;
|
|
76
76
|
border: string;
|
|
77
|
+
seriesBorderColor: string;
|
|
78
|
+
seriesConnectorColor?: string;
|
|
77
79
|
color: string;
|
|
78
80
|
fontFamily: string;
|
|
79
81
|
fontSize: number;
|
|
@@ -313,6 +315,8 @@ export type TooltipConfigBase = {
|
|
|
313
315
|
position?: TooltipPosition;
|
|
314
316
|
barAnchorPosition?: TooltipBarAnchorPosition;
|
|
315
317
|
colorMode?: TooltipColorMode;
|
|
318
|
+
seriesBorderColor?: string;
|
|
319
|
+
seriesConnectorColor?: string;
|
|
316
320
|
maxWidth?: number;
|
|
317
321
|
transition?: TooltipTransitionConfig;
|
|
318
322
|
formatter?: SeriesValueFormatter;
|
package/docs/components.md
CHANGED
|
@@ -129,6 +129,8 @@ new Tooltip({
|
|
|
129
129
|
position?: 'auto' | 'side' | 'vertical',
|
|
130
130
|
barAnchorPosition?: 'auto' | 'top' | 'middle',
|
|
131
131
|
colorMode?: 'theme' | 'series',
|
|
132
|
+
seriesBorderColor?: string,
|
|
133
|
+
seriesConnectorColor?: string,
|
|
132
134
|
maxWidth?: number, // default: 280
|
|
133
135
|
transition?: {
|
|
134
136
|
show?: boolean,
|
|
@@ -159,6 +161,11 @@ Use `position: 'auto' | 'side' | 'vertical'` for XY tooltip placement.
|
|
|
159
161
|
`auto` is the default. It uses above/below placement for horizontal XY charts
|
|
160
162
|
and side placement elsewhere. For bar tooltips, `barAnchorPosition` defaults to
|
|
161
163
|
`auto`, which aims arrows inside the visible bar segment when possible.
|
|
164
|
+
Vertical bar split tooltips aim at each bar's top edge and use series/legend
|
|
165
|
+
order to keep equal or near-equal placements stable. Very short connector
|
|
166
|
+
paths are omitted when the arrow is already close to its target. For line,
|
|
167
|
+
area, and scatter points, side split tooltips spread nearby point targets
|
|
168
|
+
within the connectorless arrow range before drawing connector paths.
|
|
162
169
|
|
|
163
170
|
For horizontal bar charts, prefer `position: 'auto'` or `position: 'vertical'`.
|
|
164
171
|
`position: 'side'` and `barAnchorPosition: 'top' | 'middle'` are kept for
|
|
@@ -167,7 +174,13 @@ legacy configs, but horizontal bars resolve bar anchoring automatically.
|
|
|
167
174
|
Tooltips default to a `280px` max width. Set `transition.show: true` to fade and
|
|
168
175
|
slide tooltips between hovered positions. Tooltip colors use `theme.tooltip` by
|
|
169
176
|
default. Set `colorMode: 'series'` to color-code XY tooltips from the series
|
|
170
|
-
color,
|
|
177
|
+
color, using series-colored backgrounds plus automatic contrast text.
|
|
178
|
+
Set `theme.tooltip.seriesBorderColor` to keep series-colored backgrounds while
|
|
179
|
+
using one custom box and arrow border color. It defaults to `'#ffffff'`.
|
|
180
|
+
Connector lines use the series color by default; set
|
|
181
|
+
`theme.tooltip.seriesConnectorColor` when they need a custom color. Tooltip
|
|
182
|
+
`seriesBorderColor` and `seriesConnectorColor` options override the theme for a
|
|
183
|
+
single tooltip component.
|
|
171
184
|
|
|
172
185
|
`defaultResponsiveConfig` switches tooltip components to `mode: 'shared'` at
|
|
173
186
|
the `sm` breakpoint so compact XY charts use one grouped tooltip by default.
|
package/docs/theming.md
CHANGED
|
@@ -30,6 +30,8 @@ const chart = new XYChart({
|
|
|
30
30
|
tooltip: {
|
|
31
31
|
background: '#102030',
|
|
32
32
|
border: '#405060',
|
|
33
|
+
seriesBorderColor: '#ffffff',
|
|
34
|
+
seriesConnectorColor: '#405060',
|
|
33
35
|
color: '#f7fafc',
|
|
34
36
|
fontFamily: 'Inter, sans-serif',
|
|
35
37
|
fontSize: 13,
|
|
@@ -66,6 +68,8 @@ through the `themes` map as `themes.default`, `themes.ruby`,
|
|
|
66
68
|
| `legend.itemSpacingY` | `number` | `8` | Vertical spacing between wrapped legend rows |
|
|
67
69
|
| `tooltip.background` | `string` | `'#ffffff'` | Tooltip box and arrow fill color |
|
|
68
70
|
| `tooltip.border` | `string` | `'#dddddd'` | Tooltip box, connector, and arrow border |
|
|
71
|
+
| `tooltip.seriesBorderColor` | `string` | `'#ffffff'` | Box and arrow border color for series-colored tooltips |
|
|
72
|
+
| `tooltip.seriesConnectorColor` | `string` | Series color | Connector line color for series-colored tooltips |
|
|
69
73
|
| `tooltip.color` | `string` | `'#1f2a36'` | Tooltip text color |
|
|
70
74
|
| `tooltip.fontFamily` | `string` | - | Tooltip text font |
|
|
71
75
|
| `tooltip.fontSize` | `number` | `12` | Tooltip text size in pixels |
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.18.
|
|
2
|
+
"version": "0.18.2",
|
|
3
3
|
"name": "@internetstiftelsen/charts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"format": "prettier --write ./src",
|
|
34
34
|
"preview": "vite preview",
|
|
35
35
|
"preview:docs": "vite preview --config vite.docs.config.ts",
|
|
36
|
-
"test": "vitest",
|
|
37
|
-
"test:run": "vitest run",
|
|
36
|
+
"test": "vitest --project unit",
|
|
37
|
+
"test:run": "vitest run --project unit",
|
|
38
38
|
"verify": "pnpm lint && pnpm test:run && pnpm build",
|
|
39
39
|
"build:lib": "npm run build",
|
|
40
40
|
"prepublishOnly": "pnpm verify",
|
|
41
|
-
"pub": "npm publish --access public"
|
|
41
|
+
"pub": "npm publish --access public",
|
|
42
|
+
"storybook": "storybook dev -p 6006",
|
|
43
|
+
"build-storybook": "storybook build"
|
|
42
44
|
},
|
|
43
45
|
"dependencies": {
|
|
44
46
|
"d3": "^7.9.0",
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
"write-excel-file": "^4.1.1"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
53
|
+
"@chromatic-com/storybook": "^5.2.1",
|
|
51
54
|
"@eslint/js": "^10.0.1",
|
|
52
55
|
"@handsontable/react-wrapper": "^17.1.0",
|
|
53
56
|
"@internetstiftelsen/styleguide": "^5.1.27",
|
|
@@ -56,6 +59,11 @@
|
|
|
56
59
|
"@radix-ui/react-switch": "^1.3.0",
|
|
57
60
|
"@radix-ui/react-tabs": "^1.1.14",
|
|
58
61
|
"@speed-highlight/core": "^1.2.17",
|
|
62
|
+
"@storybook/addon-a11y": "^10.4.6",
|
|
63
|
+
"@storybook/addon-docs": "^10.4.6",
|
|
64
|
+
"@storybook/addon-mcp": "^0.6.0",
|
|
65
|
+
"@storybook/addon-vitest": "^10.4.6",
|
|
66
|
+
"@storybook/react-vite": "^10.4.6",
|
|
59
67
|
"@tailwindcss/vite": "^4.3.1",
|
|
60
68
|
"@testing-library/dom": "^10.4.1",
|
|
61
69
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -65,21 +73,26 @@
|
|
|
65
73
|
"@types/node": "^25.9.3",
|
|
66
74
|
"@types/react": "^19.2.17",
|
|
67
75
|
"@types/react-dom": "^19.2.3",
|
|
76
|
+
"@vitest/browser-playwright": "4.1.8",
|
|
77
|
+
"@vitest/coverage-v8": "4.1.8",
|
|
68
78
|
"@vitejs/plugin-react-swc": "^4.3.1",
|
|
69
79
|
"class-variance-authority": "^0.7.1",
|
|
70
80
|
"clsx": "^2.1.1",
|
|
71
81
|
"eslint": "^10.5.0",
|
|
72
82
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
73
83
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
84
|
+
"eslint-plugin-storybook": "^10.4.6",
|
|
74
85
|
"globals": "^17.6.0",
|
|
75
86
|
"handsontable": "^17.1.0",
|
|
76
87
|
"jsdom": "^29.1.1",
|
|
77
88
|
"lucide-react": "^1.18.0",
|
|
89
|
+
"playwright": "^1.61.1",
|
|
78
90
|
"prettier": "3.8.4",
|
|
79
91
|
"radix-ui": "^1.5.0",
|
|
80
92
|
"react": "^19.2.7",
|
|
81
93
|
"react-dom": "^19.2.7",
|
|
82
94
|
"sass": "^1.101.0",
|
|
95
|
+
"storybook": "^10.4.6",
|
|
83
96
|
"tailwind-merge": "^3.6.0",
|
|
84
97
|
"tailwindcss": "^4.3.1",
|
|
85
98
|
"tsc-alias": "^1.8.17",
|