@internetstiftelsen/charts 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/index.d.ts +26 -0
- package/dist/index.js +24 -0
- 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 +124 -53
- package/dist/tooltip/types.d.ts +1 -0
- package/dist/tooltip/types.js +1 -0
- 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/getting-started.md +13 -9
- package/docs/theming.md +4 -0
- package/package.json +26 -6
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
|
|
@@ -88,10 +101,7 @@ Builds the demo app using the default Vite config.
|
|
|
88
101
|
## Quick Start
|
|
89
102
|
|
|
90
103
|
```javascript
|
|
91
|
-
import { XYChart } from '@internetstiftelsen/charts
|
|
92
|
-
import { Line } from '@internetstiftelsen/charts/line';
|
|
93
|
-
import { XAxis } from '@internetstiftelsen/charts/x-axis';
|
|
94
|
-
import { YAxis } from '@internetstiftelsen/charts/y-axis';
|
|
104
|
+
import { Line, XAxis, XYChart, YAxis } from '@internetstiftelsen/charts';
|
|
95
105
|
|
|
96
106
|
const data = [
|
|
97
107
|
{ date: '2023', revenue: 100, expenses: 80 },
|
|
@@ -109,6 +119,9 @@ chart
|
|
|
109
119
|
chart.render('#chart-container');
|
|
110
120
|
```
|
|
111
121
|
|
|
122
|
+
Subpath imports like `@internetstiftelsen/charts/xy-chart` remain available when
|
|
123
|
+
you want to import individual modules directly.
|
|
124
|
+
|
|
112
125
|
Use top-level `width` and `height` for fixed-size charts, or omit them to size
|
|
113
126
|
from the render container.
|
|
114
127
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from './area.js';
|
|
2
|
+
export * from './bar.js';
|
|
3
|
+
export * from './base-chart.js';
|
|
4
|
+
export * from './chart-group.js';
|
|
5
|
+
export * from './chart-interface.js';
|
|
6
|
+
export * from './donut-center-content.js';
|
|
7
|
+
export * from './donut-chart.js';
|
|
8
|
+
export * from './gauge-chart.js';
|
|
9
|
+
export * from './grid.js';
|
|
10
|
+
export * from './legend.js';
|
|
11
|
+
export * from './line.js';
|
|
12
|
+
export * from './pie-chart.js';
|
|
13
|
+
export * from './scatter.js';
|
|
14
|
+
export * from './text.js';
|
|
15
|
+
export * from './theme.js';
|
|
16
|
+
export * from './title.js';
|
|
17
|
+
export * from './tooltip.js';
|
|
18
|
+
export * from './types.js';
|
|
19
|
+
export * from './word-cloud-chart.js';
|
|
20
|
+
export * from './x-axis.js';
|
|
21
|
+
export * from './xy-chart.js';
|
|
22
|
+
export * from './y-axis.js';
|
|
23
|
+
export { mountChartWhenVisible } from './lazy-mount.js';
|
|
24
|
+
export type { LazyChartFactory, LazyChartMountHandle, LazyChartMountOptions, LazyMountableChart, } from './lazy-mount.js';
|
|
25
|
+
export { toChartData } from './utils.js';
|
|
26
|
+
export type { GroupedStringParseOptions, ToChartDataOptions } from './utils.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export * from './area.js';
|
|
2
|
+
export * from './bar.js';
|
|
3
|
+
export * from './base-chart.js';
|
|
4
|
+
export * from './chart-group.js';
|
|
5
|
+
export * from './chart-interface.js';
|
|
6
|
+
export * from './donut-center-content.js';
|
|
7
|
+
export * from './donut-chart.js';
|
|
8
|
+
export * from './gauge-chart.js';
|
|
9
|
+
export * from './grid.js';
|
|
10
|
+
export * from './legend.js';
|
|
11
|
+
export * from './line.js';
|
|
12
|
+
export * from './pie-chart.js';
|
|
13
|
+
export * from './scatter.js';
|
|
14
|
+
export * from './text.js';
|
|
15
|
+
export * from './theme.js';
|
|
16
|
+
export * from './title.js';
|
|
17
|
+
export * from './tooltip.js';
|
|
18
|
+
export * from './types.js';
|
|
19
|
+
export * from './word-cloud-chart.js';
|
|
20
|
+
export * from './x-axis.js';
|
|
21
|
+
export * from './xy-chart.js';
|
|
22
|
+
export * from './y-axis.js';
|
|
23
|
+
export { mountChartWhenVisible } from './lazy-mount.js';
|
|
24
|
+
export { toChartData } from './utils.js';
|
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,4 +1,4 @@
|
|
|
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
4
|
export function getSplitTooltipViewportBounds() {
|
|
@@ -175,7 +175,7 @@ export function resolveSplitTooltipPositions(layouts, position, bounds = getSpli
|
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
const arrowEdge = resolveSidePlacementArrowEdge(getCombinedSplitTooltipAnchor(layouts), Math.max(...layouts.map((layout) => layout.width)), bounds);
|
|
178
|
-
const orderedLayouts =
|
|
178
|
+
const orderedLayouts = getSideSplitTooltipLayouts(layouts);
|
|
179
179
|
positionSideSplitTooltipStack(orderedLayouts, arrowEdge, bounds);
|
|
180
180
|
}
|
|
181
181
|
function resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
@@ -212,7 +212,7 @@ function getTooltipConnectorOffset(start, size, target) {
|
|
|
212
212
|
}
|
|
213
213
|
function resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
|
|
214
214
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
215
|
-
if (
|
|
215
|
+
if (isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor)) {
|
|
216
216
|
return '';
|
|
217
217
|
}
|
|
218
218
|
const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
@@ -232,6 +232,13 @@ function isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor) {
|
|
|
232
232
|
return (arrowTipY >= anchor.top - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
233
233
|
arrowTipY <= anchor.bottom + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
234
234
|
}
|
|
235
|
+
function isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor) {
|
|
236
|
+
if (isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return (arrowTipY >= anchor.top - TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX &&
|
|
240
|
+
arrowTipY <= anchor.bottom + TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX);
|
|
241
|
+
}
|
|
235
242
|
function resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
|
|
236
243
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
237
244
|
return {
|
|
@@ -278,6 +285,14 @@ function getCombinedSplitTooltipTarget(layouts) {
|
|
|
278
285
|
y: (minY + maxY) / 2,
|
|
279
286
|
};
|
|
280
287
|
}
|
|
288
|
+
function getSideSplitTooltipLayouts(layouts) {
|
|
289
|
+
if (layouts.every((layout) => layout.targetMode === 'auto')) {
|
|
290
|
+
return [...layouts].sort((a, b) => a.anchor.top - b.anchor.top ||
|
|
291
|
+
a.anchor.left - b.anchor.left ||
|
|
292
|
+
a.order - b.order);
|
|
293
|
+
}
|
|
294
|
+
return getVerticallyOrderedSplitTooltipLayouts(layouts);
|
|
295
|
+
}
|
|
281
296
|
function resolveHorizontalSideSplitTooltipPositions(layouts, bounds) {
|
|
282
297
|
const rowPlacement = getBestHorizontalSideRowPlacement(getHorizontallyOrderedSplitTooltipLayouts(layouts), bounds);
|
|
283
298
|
rowPlacement.placements.forEach(({ layout, left, top, arrowEdge }) => {
|
|
@@ -424,6 +439,7 @@ function getHorizontalAboveBelowRowPlacement(layouts, assignment, preferredEdges
|
|
|
424
439
|
const rowLayouts = layouts.filter((_, index) => assignment[index] === arrowEdge);
|
|
425
440
|
return packHorizontalAboveBelowRow(rowLayouts, arrowEdge, bounds);
|
|
426
441
|
});
|
|
442
|
+
separateOverlappingHorizontalTooltipRows(placements, bounds);
|
|
427
443
|
const preferredEdgeByLayout = new Map(layouts.map((layout, index) => [layout, preferredEdges[index]]));
|
|
428
444
|
const score = placements.reduce((sum, placement) => {
|
|
429
445
|
const preferredEdge = preferredEdgeByLayout.get(placement.layout) ??
|
|
@@ -493,6 +509,59 @@ function packHorizontalTooltipRow(placements, bounds) {
|
|
|
493
509
|
}
|
|
494
510
|
return placements;
|
|
495
511
|
}
|
|
512
|
+
function separateOverlappingHorizontalTooltipRows(placements, bounds) {
|
|
513
|
+
if (placements.length < 2) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const orderedPlacements = [...placements].sort((a, b) => a.top - b.top ||
|
|
517
|
+
a.left - b.left ||
|
|
518
|
+
a.layout.targetX - b.layout.targetX ||
|
|
519
|
+
a.layout.order - b.layout.order);
|
|
520
|
+
for (let index = 0; index < orderedPlacements.length; index++) {
|
|
521
|
+
const placement = orderedPlacements[index];
|
|
522
|
+
for (let previousIndex = 0; previousIndex < index; previousIndex++) {
|
|
523
|
+
const previousPlacement = orderedPlacements[previousIndex];
|
|
524
|
+
if (!doHorizontalTooltipPlacementsOverlap(placement, previousPlacement)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
placement.top = Math.max(placement.top, previousPlacement.top +
|
|
528
|
+
previousPlacement.layout.height +
|
|
529
|
+
SPLIT_TOOLTIP_GAP_PX);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const overflow = Math.max(...orderedPlacements.map((placement) => placement.top + placement.layout.height)) - bounds.maxBottom;
|
|
533
|
+
if (overflow > 0) {
|
|
534
|
+
orderedPlacements.forEach((placement) => {
|
|
535
|
+
placement.top -= overflow;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
for (let index = orderedPlacements.length - 2; index >= 0; index--) {
|
|
539
|
+
const placement = orderedPlacements[index];
|
|
540
|
+
for (let nextIndex = orderedPlacements.length - 1; nextIndex > index; nextIndex--) {
|
|
541
|
+
const nextPlacement = orderedPlacements[nextIndex];
|
|
542
|
+
if (!doHorizontalTooltipPlacementsOverlap(placement, nextPlacement)) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
placement.top = Math.min(placement.top, nextPlacement.top -
|
|
546
|
+
placement.layout.height -
|
|
547
|
+
SPLIT_TOOLTIP_GAP_PX);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const underflow = bounds.minTop -
|
|
551
|
+
Math.min(...orderedPlacements.map((placement) => placement.top));
|
|
552
|
+
if (underflow <= 0) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
orderedPlacements.forEach((placement) => {
|
|
556
|
+
placement.top += underflow;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function doHorizontalTooltipPlacementsOverlap(first, second) {
|
|
560
|
+
return (first.left < second.left + second.layout.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
561
|
+
first.left + first.layout.width + SPLIT_TOOLTIP_GAP_PX > second.left &&
|
|
562
|
+
first.top < second.top + second.layout.height + SPLIT_TOOLTIP_GAP_PX &&
|
|
563
|
+
first.top + first.layout.height + SPLIT_TOOLTIP_GAP_PX > second.top);
|
|
564
|
+
}
|
|
496
565
|
function getHorizontalAboveBelowIdealLeft(layout) {
|
|
497
566
|
const centeredLeft = layout.targetX - layout.width / 2;
|
|
498
567
|
if (layout.targetMode === 'auto') {
|
|
@@ -513,8 +582,13 @@ function getHorizontalAboveBelowPlacementCost(candidate, layout, preferredEdge,
|
|
|
513
582
|
? 0
|
|
514
583
|
: layout.height * 4;
|
|
515
584
|
const xDistance = Math.abs(layout.targetX - (candidate.left + layout.width / 2));
|
|
585
|
+
const yDistance = Math.abs(candidate.top - getHorizontalAboveBelowTop(layout, candidate.arrowEdge));
|
|
516
586
|
const overflowPenalty = getSplitTooltipBoxPlacementOverflow(candidate, layout, bounds) * 20;
|
|
517
|
-
return connectorPenalty +
|
|
587
|
+
return (connectorPenalty +
|
|
588
|
+
edgePenalty +
|
|
589
|
+
xDistance +
|
|
590
|
+
yDistance * 0.5 +
|
|
591
|
+
overflowPenalty);
|
|
518
592
|
}
|
|
519
593
|
function getPreferredAndOppositeEdges(preferredEdge) {
|
|
520
594
|
if (preferredEdge === 'bottom') {
|
|
@@ -544,28 +618,14 @@ function getSplitTooltipBoxPlacementOverflow(candidate, layout, bounds) {
|
|
|
544
618
|
Math.max(0, candidate.top + layout.height - bounds.maxBottom));
|
|
545
619
|
}
|
|
546
620
|
function positionSideSplitTooltipStack(layouts, arrowEdge, bounds) {
|
|
547
|
-
const placementOrder = getSidePlacementOrder(layouts);
|
|
548
621
|
const placedLayouts = [];
|
|
549
|
-
|
|
622
|
+
layouts.forEach((layout) => {
|
|
550
623
|
layout.arrowEdge = arrowEdge;
|
|
551
624
|
layout.left = getSideTooltipLeft(layout, arrowEdge, bounds);
|
|
552
625
|
positionSideSplitTooltip(layout, placedLayouts, layouts, bounds);
|
|
553
626
|
placedLayouts.push(layout);
|
|
554
627
|
});
|
|
555
628
|
}
|
|
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
629
|
function getSideTooltipLeft(layout, arrowEdge, bounds) {
|
|
570
630
|
const preferredLeft = arrowEdge === 'left'
|
|
571
631
|
? layout.anchor.right + TOOLTIP_OFFSET_PX
|
|
@@ -630,53 +690,64 @@ function getSidePlacementCandidates(layout, placedLayouts, allLayouts, bounds) {
|
|
|
630
690
|
getSidePlacementCost(b, layout, allLayouts));
|
|
631
691
|
}
|
|
632
692
|
function getSidePlacementCost(candidate, layout, allLayouts) {
|
|
633
|
-
const
|
|
634
|
-
? 0
|
|
635
|
-
: layout.height * 4;
|
|
693
|
+
const connectorless = isSidePlacementConnectorless(candidate, layout);
|
|
636
694
|
const centerDistance = Math.abs(layout.targetY - (candidate.top + layout.height / 2));
|
|
637
|
-
|
|
638
|
-
|
|
695
|
+
const arrowDistance = Math.abs(layout.targetY - getSidePlacementArrowTipY(candidate.top, layout));
|
|
696
|
+
if (layout.targetMode === 'auto' && connectorless) {
|
|
697
|
+
const preferredTop = layout.targetY - getTooltipConnectorMaxOffset(layout.height);
|
|
698
|
+
const preferredTopDistance = Math.abs(candidate.top - preferredTop);
|
|
699
|
+
return preferredTopDistance + arrowDistance * 0.1;
|
|
700
|
+
}
|
|
701
|
+
if (connectorless) {
|
|
702
|
+
const spreadDirection = getFixedPointSideSpreadDirection(layout, allLayouts);
|
|
639
703
|
if (spreadDirection !== 0) {
|
|
640
|
-
const
|
|
641
|
-
const preferredTop = spreadDirection > 0
|
|
642
|
-
? connectorlessRange.max
|
|
643
|
-
: connectorlessRange.min;
|
|
704
|
+
const preferredTop = getFixedPointSideSpreadPreferredTop(layout, spreadDirection);
|
|
644
705
|
return (Math.abs(candidate.top - preferredTop) + centerDistance * 0.05);
|
|
645
706
|
}
|
|
707
|
+
return centerDistance;
|
|
646
708
|
}
|
|
647
|
-
return
|
|
709
|
+
return layout.height * 4 + arrowDistance + centerDistance * 0.2;
|
|
648
710
|
}
|
|
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) {
|
|
711
|
+
function getFixedPointSideSpreadDirection(layout, allLayouts) {
|
|
712
|
+
if (!isFixedPointSideTooltipLayout(layout)) {
|
|
659
713
|
return 0;
|
|
660
714
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
715
|
+
const nearbyLayouts = allLayouts
|
|
716
|
+
.filter((otherLayout) => isFixedPointSideTooltipLayout(otherLayout) &&
|
|
717
|
+
areFixedPointSideTargetsNear(layout, otherLayout))
|
|
718
|
+
.sort((a, b) => a.targetY - b.targetY ||
|
|
719
|
+
a.targetX - b.targetX ||
|
|
720
|
+
a.order - b.order);
|
|
721
|
+
if (nearbyLayouts.length < 2) {
|
|
722
|
+
return 0;
|
|
665
723
|
}
|
|
666
|
-
|
|
724
|
+
const layoutIndex = nearbyLayouts.indexOf(layout);
|
|
725
|
+
const clusterCenterIndex = (nearbyLayouts.length - 1) / 2;
|
|
726
|
+
if (layoutIndex < clusterCenterIndex) {
|
|
667
727
|
return -1;
|
|
668
728
|
}
|
|
729
|
+
if (layoutIndex > clusterCenterIndex) {
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
669
732
|
return 0;
|
|
670
733
|
}
|
|
671
|
-
function
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
734
|
+
function isFixedPointSideTooltipLayout(layout) {
|
|
735
|
+
return (layout.targetMode === 'fixed' &&
|
|
736
|
+
Math.abs(layout.anchor.left - layout.anchor.right) <=
|
|
737
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
738
|
+
Math.abs(layout.anchor.top - layout.anchor.bottom) <=
|
|
739
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
740
|
+
}
|
|
741
|
+
function areFixedPointSideTargetsNear(layout, otherLayout) {
|
|
742
|
+
const centeredTooltipsOverlapDistance = (layout.height + otherLayout.height) / 2 + SPLIT_TOOLTIP_GAP_PX;
|
|
743
|
+
return (Math.abs(layout.targetY - otherLayout.targetY) <
|
|
744
|
+
centeredTooltipsOverlapDistance);
|
|
745
|
+
}
|
|
746
|
+
function getFixedPointSideSpreadPreferredTop(layout, direction) {
|
|
747
|
+
if (direction < 0) {
|
|
748
|
+
return layout.targetY - getTooltipConnectorMaxOffset(layout.height);
|
|
749
|
+
}
|
|
750
|
+
return layout.targetY - TOOLTIP_CONNECTOR_INSET_PX;
|
|
680
751
|
}
|
|
681
752
|
function getSideConnectorlessTopRange(layout) {
|
|
682
753
|
return {
|
package/dist/tooltip/types.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
export declare const TOOLTIP_CONNECTOR_INSET_PX = 14;
|
|
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;
|
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
3
|
export const TOOLTIP_CONNECTOR_INSET_PX = 14;
|
|
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;
|
|
@@ -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/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/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,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.18.1",
|
|
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",
|
|
@@ -26,12 +33,14 @@
|
|
|
26
33
|
"format": "prettier --write ./src",
|
|
27
34
|
"preview": "vite preview",
|
|
28
35
|
"preview:docs": "vite preview --config vite.docs.config.ts",
|
|
29
|
-
"test": "vitest",
|
|
30
|
-
"test:run": "vitest run",
|
|
36
|
+
"test": "vitest --project unit",
|
|
37
|
+
"test:run": "vitest run --project unit",
|
|
31
38
|
"verify": "pnpm lint && pnpm test:run && pnpm build",
|
|
32
39
|
"build:lib": "npm run build",
|
|
33
40
|
"prepublishOnly": "pnpm verify",
|
|
34
|
-
"pub": "npm publish --access public"
|
|
41
|
+
"pub": "npm publish --access public",
|
|
42
|
+
"storybook": "storybook dev -p 6006",
|
|
43
|
+
"build-storybook": "storybook build"
|
|
35
44
|
},
|
|
36
45
|
"dependencies": {
|
|
37
46
|
"d3": "^7.9.0",
|
|
@@ -41,6 +50,7 @@
|
|
|
41
50
|
"write-excel-file": "^4.1.1"
|
|
42
51
|
},
|
|
43
52
|
"devDependencies": {
|
|
53
|
+
"@chromatic-com/storybook": "^5.2.1",
|
|
44
54
|
"@eslint/js": "^10.0.1",
|
|
45
55
|
"@handsontable/react-wrapper": "^17.1.0",
|
|
46
56
|
"@internetstiftelsen/styleguide": "^5.1.27",
|
|
@@ -49,6 +59,11 @@
|
|
|
49
59
|
"@radix-ui/react-switch": "^1.3.0",
|
|
50
60
|
"@radix-ui/react-tabs": "^1.1.14",
|
|
51
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",
|
|
52
67
|
"@tailwindcss/vite": "^4.3.1",
|
|
53
68
|
"@testing-library/dom": "^10.4.1",
|
|
54
69
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -58,21 +73,26 @@
|
|
|
58
73
|
"@types/node": "^25.9.3",
|
|
59
74
|
"@types/react": "^19.2.17",
|
|
60
75
|
"@types/react-dom": "^19.2.3",
|
|
76
|
+
"@vitest/browser-playwright": "4.1.8",
|
|
77
|
+
"@vitest/coverage-v8": "4.1.8",
|
|
61
78
|
"@vitejs/plugin-react-swc": "^4.3.1",
|
|
62
79
|
"class-variance-authority": "^0.7.1",
|
|
63
80
|
"clsx": "^2.1.1",
|
|
64
81
|
"eslint": "^10.5.0",
|
|
65
82
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
66
83
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
84
|
+
"eslint-plugin-storybook": "^10.4.6",
|
|
67
85
|
"globals": "^17.6.0",
|
|
68
86
|
"handsontable": "^17.1.0",
|
|
69
87
|
"jsdom": "^29.1.1",
|
|
70
88
|
"lucide-react": "^1.18.0",
|
|
89
|
+
"playwright": "^1.61.1",
|
|
71
90
|
"prettier": "3.8.4",
|
|
72
91
|
"radix-ui": "^1.5.0",
|
|
73
92
|
"react": "^19.2.7",
|
|
74
93
|
"react-dom": "^19.2.7",
|
|
75
94
|
"sass": "^1.101.0",
|
|
95
|
+
"storybook": "^10.4.6",
|
|
76
96
|
"tailwind-merge": "^3.6.0",
|
|
77
97
|
"tailwindcss": "^4.3.1",
|
|
78
98
|
"tsc-alias": "^1.8.17",
|