@internetstiftelsen/charts 0.19.3 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/area.d.ts +1 -1
- package/dist/bar.d.ts +1 -1
- package/dist/base-chart.d.ts +2 -0
- package/dist/base-chart.js +16 -12
- package/dist/donut-center-content.d.ts +1 -1
- package/dist/grid.d.ts +1 -1
- package/dist/legend.d.ts +1 -1
- package/dist/line.d.ts +1 -1
- package/dist/scatter.d.ts +1 -1
- package/dist/title.d.ts +2 -2
- package/dist/tooltip/dom.d.ts +3 -0
- package/dist/tooltip/dom.js +19 -0
- package/dist/tooltip/xy-interaction.js +58 -36
- package/dist/tooltip.d.ts +1 -1
- package/dist/word-cloud-chart.d.ts +8 -0
- package/dist/word-cloud-chart.js +145 -8
- package/dist/x-axis.d.ts +1 -1
- package/dist/y-axis.d.ts +1 -1
- package/docs/components.md +5 -0
- package/docs/word-cloud-chart.md +17 -1
- package/package.json +42 -41
package/README.md
CHANGED
|
@@ -396,9 +396,14 @@ chart.render('#word-cloud');
|
|
|
396
396
|
```
|
|
397
397
|
|
|
398
398
|
`minFontSize` and `maxFontSize` are percentages of the smaller plot-area
|
|
399
|
-
dimension and define the
|
|
399
|
+
dimension and define the initial size range passed into `d3-cloud`. The chart
|
|
400
400
|
expects flat `{ word, count }` rows, aggregates duplicate words after trimming,
|
|
401
401
|
and maps theme typography and colors directly into the layout and rendered SVG.
|
|
402
|
+
Long phrases are fitted to the available area, with at most six placement
|
|
403
|
+
attempts. Container resizing is debounced by 150 ms; initial rendering and data
|
|
404
|
+
updates remain immediate. If space remains insufficient, the chart retains the highest-frequency
|
|
405
|
+
words without shrinking below the configured minimum. See the
|
|
406
|
+
[WordCloudChart API](./docs/word-cloud-chart.md#notes) for fitting behavior.
|
|
402
407
|
Set `animate: true` or pass an animation config to fade and scale words from
|
|
403
408
|
their own centers on initial render and `chart.update(...)`.
|
|
404
409
|
|
package/dist/area.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AreaConfig, AreaCurveType, AreaConfigBase, AreaStackingContext, Ch
|
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
import type { XYAreaAnimationContext, XYAreaPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
5
5
|
export declare class Area implements ChartComponent<AreaConfigBase> {
|
|
6
|
-
readonly type:
|
|
6
|
+
readonly type: 'area';
|
|
7
7
|
readonly dataKey: string;
|
|
8
8
|
readonly fill: string;
|
|
9
9
|
readonly stroke: string;
|
package/dist/bar.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { BarConfig, BarStackingContext, BarSide, BarValueLabelConfig, Chart
|
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
import type { XYBarAnimationContext, XYBarSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
5
5
|
export declare class Bar implements ChartComponent<BarConfigBase> {
|
|
6
|
-
readonly type:
|
|
6
|
+
readonly type: 'bar';
|
|
7
7
|
readonly dataKey: string;
|
|
8
8
|
readonly fill: string;
|
|
9
9
|
readonly colorAdapter?: (data: DataItem, index: number) => string;
|
package/dist/base-chart.d.ts
CHANGED
|
@@ -213,6 +213,8 @@ export declare abstract class BaseChart {
|
|
|
213
213
|
* Setup ResizeObserver for automatic resize handling
|
|
214
214
|
*/
|
|
215
215
|
private setupResizeObserver;
|
|
216
|
+
protected hasContainerSizeChanged(): boolean;
|
|
217
|
+
protected handleResize(): void;
|
|
216
218
|
private notifyLegendChanged;
|
|
217
219
|
/**
|
|
218
220
|
* Subclasses must implement this method to define their rendering logic
|
package/dist/base-chart.js
CHANGED
|
@@ -939,19 +939,22 @@ export class BaseChart {
|
|
|
939
939
|
if (this.resizeObserver) {
|
|
940
940
|
this.resizeObserver.disconnect();
|
|
941
941
|
}
|
|
942
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
943
|
-
if (!this.container) {
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
const nextDimensions = this.resolveRenderDimensions(this.container.getBoundingClientRect());
|
|
947
|
-
if (nextDimensions.width === this.width &&
|
|
948
|
-
nextDimensions.height === this.height) {
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
this.rerender('resize');
|
|
952
|
-
});
|
|
942
|
+
this.resizeObserver = new ResizeObserver(() => this.handleResize());
|
|
953
943
|
this.resizeObserver.observe(this.container);
|
|
954
944
|
}
|
|
945
|
+
hasContainerSizeChanged() {
|
|
946
|
+
if (!this.container) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
const nextDimensions = this.resolveRenderDimensions(this.container.getBoundingClientRect());
|
|
950
|
+
return (nextDimensions.width !== this.width ||
|
|
951
|
+
nextDimensions.height !== this.height);
|
|
952
|
+
}
|
|
953
|
+
handleResize() {
|
|
954
|
+
if (this.hasContainerSizeChanged()) {
|
|
955
|
+
this.rerender('resize');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
955
958
|
notifyLegendChanged() {
|
|
956
959
|
this.emit('legend:change');
|
|
957
960
|
}
|
|
@@ -1310,6 +1313,7 @@ export class BaseChart {
|
|
|
1310
1313
|
return exportXLSXBlob(this.sourceData, options);
|
|
1311
1314
|
}
|
|
1312
1315
|
async exportImage(format, options) {
|
|
1316
|
+
await this.whenReady();
|
|
1313
1317
|
const { width, height } = this.exportSize(options);
|
|
1314
1318
|
const svg = await this.exportSVG(options, format);
|
|
1315
1319
|
const backgroundColor = options?.backgroundColor ??
|
|
@@ -1325,8 +1329,8 @@ export class BaseChart {
|
|
|
1325
1329
|
});
|
|
1326
1330
|
}
|
|
1327
1331
|
async exportSVG(options, formatForHooks = 'svg') {
|
|
1328
|
-
const liveSvg = this.requireRenderedSvg();
|
|
1329
1332
|
await this.whenReady();
|
|
1333
|
+
const liveSvg = this.requireRenderedSvg();
|
|
1330
1334
|
const { exportWidth, exportHeight, requiresExportRender, baseContext } = this.resolveExportContext(options, formatForHooks);
|
|
1331
1335
|
const clone = this.createExportSvgClone(liveSvg, exportWidth, exportHeight);
|
|
1332
1336
|
const overrides = this.collectExportOverrides(baseContext);
|
|
@@ -19,7 +19,7 @@ export type DonutCenterContentConfig = DonutCenterContentConfigBase & {
|
|
|
19
19
|
exportHooks?: ExportHooks<DonutCenterContentConfigBase>;
|
|
20
20
|
};
|
|
21
21
|
export declare class DonutCenterContent implements ChartComponent<DonutCenterContentConfigBase> {
|
|
22
|
-
readonly type:
|
|
22
|
+
readonly type: 'donutCenterContent';
|
|
23
23
|
readonly mainValue?: string;
|
|
24
24
|
readonly title?: string;
|
|
25
25
|
readonly subtitle?: string;
|
package/dist/grid.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Selection } from 'd3';
|
|
|
2
2
|
import type { GridConfig, ChartTheme, D3Scale, ExportHooks, GridConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
export declare class Grid implements ChartComponent<GridConfigBase> {
|
|
5
|
-
readonly type:
|
|
5
|
+
readonly type: 'grid';
|
|
6
6
|
readonly value: boolean;
|
|
7
7
|
readonly category: boolean;
|
|
8
8
|
readonly exportHooks?: ExportHooks<GridConfigBase>;
|
package/dist/legend.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { LegendConfig, ChartTheme, HorizontalAlignment, LegendSeries, Expor
|
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
4
|
import { LegendStateController } from './legend-state.js';
|
|
5
5
|
export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
6
|
-
readonly type:
|
|
6
|
+
readonly type: 'legend';
|
|
7
7
|
mode: LegendMode;
|
|
8
8
|
readonly position: LegendConfig['position'];
|
|
9
9
|
readonly disconnectedTarget?: string | HTMLElement;
|
package/dist/line.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLab
|
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
import type { XYPointAnimationContext, XYPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
5
5
|
export declare class Line implements ChartComponent<LineConfigBase> {
|
|
6
|
-
readonly type:
|
|
6
|
+
readonly type: 'line';
|
|
7
7
|
readonly dataKey: string;
|
|
8
8
|
readonly stroke: string;
|
|
9
9
|
readonly strokeWidth?: number;
|
package/dist/scatter.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ChartComponent } from './chart-interface.js';
|
|
|
3
3
|
import type { Selection } from 'd3';
|
|
4
4
|
import type { XYPointAnimationContext, XYPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
5
5
|
export declare class Scatter implements ChartComponent<ScatterConfigBase> {
|
|
6
|
-
readonly type:
|
|
6
|
+
readonly type: 'scatter';
|
|
7
7
|
readonly dataKey: string;
|
|
8
8
|
readonly stroke: string;
|
|
9
9
|
readonly pointSize?: number;
|
package/dist/title.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { type Selection } from 'd3';
|
|
|
2
2
|
import type { ChartTheme, ExportHooks, TitleConfig, TitleConfigBase } from './types.js';
|
|
3
3
|
import type { ComponentSpace, LayoutAwareComponent } from './chart-interface.js';
|
|
4
4
|
export declare class Title implements LayoutAwareComponent<TitleConfigBase> {
|
|
5
|
-
readonly type:
|
|
5
|
+
readonly type: 'title';
|
|
6
6
|
readonly display: boolean;
|
|
7
7
|
readonly text: string;
|
|
8
|
-
readonly position:
|
|
8
|
+
readonly position: 'top';
|
|
9
9
|
readonly variant = "title";
|
|
10
10
|
readonly exportHooks?: ExportHooks<TitleConfigBase>;
|
|
11
11
|
private readonly textComponent;
|
package/dist/tooltip/dom.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare class TooltipDom {
|
|
|
27
27
|
setContent(content: string): void;
|
|
28
28
|
getBounds(): DOMRect | null;
|
|
29
29
|
showAt(left: number, top: number): void;
|
|
30
|
+
hasVisibleTooltips(): boolean;
|
|
31
|
+
moveVisibleTooltips(deltaX: number, deltaY: number): void;
|
|
30
32
|
hide(): void;
|
|
31
33
|
cleanup(): void;
|
|
32
34
|
measureTooltip(tooltip: TooltipDivSelection, content: string): {
|
|
@@ -50,6 +52,7 @@ export declare class TooltipDom {
|
|
|
50
52
|
private scheduleTooltipPositionReset;
|
|
51
53
|
private cancelTooltipPositionReset;
|
|
52
54
|
private getTooltipTransitionStyle;
|
|
55
|
+
private getTooltipNodes;
|
|
53
56
|
private isTooltipVisible;
|
|
54
57
|
private getTooltipPosition;
|
|
55
58
|
private hasVisibleSlideOffset;
|
package/dist/tooltip/dom.js
CHANGED
|
@@ -108,6 +108,22 @@ export class TooltipDom {
|
|
|
108
108
|
}
|
|
109
109
|
this.showTooltipAt(this.tooltipDiv, left, top);
|
|
110
110
|
}
|
|
111
|
+
hasVisibleTooltips() {
|
|
112
|
+
return this.getTooltipNodes().some((node) => this.isTooltipVisible(node));
|
|
113
|
+
}
|
|
114
|
+
moveVisibleTooltips(deltaX, deltaY) {
|
|
115
|
+
this.getTooltipNodes().forEach((node) => {
|
|
116
|
+
if (!this.isTooltipVisible(node)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const position = this.getTooltipPosition(node);
|
|
120
|
+
if (!position) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
node.style.left = `${position.left + deltaX}px`;
|
|
124
|
+
node.style.top = `${position.top + deltaY}px`;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
111
127
|
hide() {
|
|
112
128
|
const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
|
|
113
129
|
if (!tooltip.empty()) {
|
|
@@ -350,6 +366,9 @@ export class TooltipDom {
|
|
|
350
366
|
getTooltipTransitionStyle() {
|
|
351
367
|
return `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`;
|
|
352
368
|
}
|
|
369
|
+
getTooltipNodes() {
|
|
370
|
+
return Array.from(document.querySelectorAll(`#${this.id}, [data-chart-tooltip-owner="${this.splitTooltipOwner}"]`));
|
|
371
|
+
}
|
|
353
372
|
isTooltipVisible(node) {
|
|
354
373
|
return (node.style.visibility === 'visible' && node.style.opacity !== '0');
|
|
355
374
|
}
|
|
@@ -301,11 +301,11 @@ export function attachXYTooltipArea(config) {
|
|
|
301
301
|
const svgNode = svg.node();
|
|
302
302
|
if (!svgNode) {
|
|
303
303
|
dom.hideTooltipSelection(tooltip);
|
|
304
|
-
return
|
|
304
|
+
return;
|
|
305
305
|
}
|
|
306
306
|
const placementBounds = getTooltipPlacementBounds(svgNode);
|
|
307
307
|
const candidates = [];
|
|
308
|
-
series.forEach((currentSeries
|
|
308
|
+
series.forEach((currentSeries) => {
|
|
309
309
|
const rawValue = dataPoint[currentSeries.dataKey];
|
|
310
310
|
if (rawValue === null || rawValue === undefined) {
|
|
311
311
|
return;
|
|
@@ -319,57 +319,90 @@ export function attachXYTooltipArea(config) {
|
|
|
319
319
|
const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
|
|
320
320
|
candidates.push({
|
|
321
321
|
series: currentSeries,
|
|
322
|
-
seriesIndex,
|
|
323
322
|
anchor: visibleAnchor,
|
|
324
323
|
target,
|
|
325
324
|
});
|
|
326
325
|
});
|
|
327
|
-
const selectedCandidate =
|
|
326
|
+
const selectedCandidate = getClosestSingleTooltipCandidate(candidates, request);
|
|
328
327
|
updateVisualStateAtIndex(request.index);
|
|
329
328
|
dom.hideSplitTooltips();
|
|
330
329
|
if (!selectedCandidate) {
|
|
331
330
|
dom.hideTooltipSelection(tooltip);
|
|
332
|
-
return
|
|
331
|
+
return;
|
|
333
332
|
}
|
|
334
333
|
dom.applyRootTooltipStyles(theme, getSeriesTooltipStyle(selectedCandidate.series, dataPoint, request.index));
|
|
335
334
|
const content = buildSplitTooltipContent(dataPoint, selectedCandidate.series);
|
|
336
335
|
const measuredTooltip = dom.measureTooltip(tooltip, content);
|
|
337
336
|
if (!measuredTooltip) {
|
|
338
337
|
dom.hideTooltipSelection(tooltip);
|
|
339
|
-
return
|
|
338
|
+
return;
|
|
340
339
|
}
|
|
341
340
|
const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, placementBounds);
|
|
342
341
|
const tooltipPosition = getAnchoredTooltipPosition(selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
|
|
343
342
|
if (!tooltipPosition) {
|
|
344
343
|
dom.hideTooltipSelection(tooltip);
|
|
345
|
-
return
|
|
344
|
+
return;
|
|
346
345
|
}
|
|
347
346
|
dom.renderTooltipWithConnector(tooltip, arrowEdge, tooltipPosition.left, tooltipPosition.top, measuredTooltip.width, measuredTooltip.height, selectedCandidate.target.x, selectedCandidate.target.y, selectedCandidate.anchor);
|
|
348
|
-
return {
|
|
349
|
-
index: request.index,
|
|
350
|
-
seriesIndex: selectedCandidate.seriesIndex,
|
|
351
|
-
};
|
|
352
347
|
};
|
|
353
|
-
|
|
348
|
+
const tooltipSvgNode = svg.node();
|
|
349
|
+
let activeSvgBounds = null;
|
|
350
|
+
let positionTrackingFrame = null;
|
|
351
|
+
const stopPositionTracking = () => {
|
|
352
|
+
activeSvgBounds = null;
|
|
353
|
+
if (positionTrackingFrame !== null) {
|
|
354
|
+
cancelFrame(positionTrackingFrame);
|
|
355
|
+
positionTrackingFrame = null;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const updateTooltipPosition = () => {
|
|
359
|
+
if (!activeSvgBounds || !tooltipSvgNode) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const nextSvgBounds = getElementTooltipBounds(tooltipSvgNode);
|
|
363
|
+
const deltaX = nextSvgBounds.minLeft - activeSvgBounds.minLeft;
|
|
364
|
+
const deltaY = nextSvgBounds.minTop - activeSvgBounds.minTop;
|
|
365
|
+
if (deltaX === 0 && deltaY === 0) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
dom.moveVisibleTooltips(deltaX, deltaY);
|
|
369
|
+
activeSvgBounds = nextSvgBounds;
|
|
370
|
+
};
|
|
371
|
+
const trackTooltipPosition = () => {
|
|
372
|
+
positionTrackingFrame = null;
|
|
373
|
+
if (!tooltipSvgNode?.isConnected || !dom.hasVisibleTooltips()) {
|
|
374
|
+
stopPositionTracking();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Scroll events can arrive late during touch or momentum scrolling.
|
|
378
|
+
updateTooltipPosition();
|
|
379
|
+
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
380
|
+
};
|
|
354
381
|
const hideTooltip = () => {
|
|
355
|
-
|
|
382
|
+
stopPositionTracking();
|
|
356
383
|
dom.hideTooltipSelection(tooltip);
|
|
357
384
|
dom.hideSplitTooltips();
|
|
358
385
|
clearVisualState();
|
|
359
386
|
};
|
|
360
|
-
const
|
|
361
|
-
|
|
387
|
+
const renderTooltip = (request) => {
|
|
388
|
+
if (!tooltipSvgNode) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
activeSvgBounds = getElementTooltipBounds(tooltipSvgNode);
|
|
392
|
+
if (positionTrackingFrame === null) {
|
|
393
|
+
positionTrackingFrame = requestFrame(trackTooltipPosition);
|
|
394
|
+
}
|
|
362
395
|
if (mode === 'single') {
|
|
363
|
-
|
|
396
|
+
showSingleTooltip(request);
|
|
364
397
|
return;
|
|
365
398
|
}
|
|
366
|
-
activeTooltipRequest = { index: request.index };
|
|
367
399
|
if (mode === 'split') {
|
|
368
400
|
showSplitTooltipAtIndex(request.index);
|
|
369
401
|
return;
|
|
370
402
|
}
|
|
371
403
|
showSharedTooltipAtIndex(request.index);
|
|
372
|
-
}
|
|
404
|
+
};
|
|
405
|
+
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, renderTooltip);
|
|
373
406
|
overlay
|
|
374
407
|
.on('mousemove', (event) => {
|
|
375
408
|
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
@@ -415,18 +448,7 @@ export function attachXYTooltipArea(config) {
|
|
|
415
448
|
}
|
|
416
449
|
queuedRender.cancel();
|
|
417
450
|
select(this).attr('stroke', '#111827').attr('stroke-width', 2);
|
|
418
|
-
|
|
419
|
-
activeTooltipRequest = showSingleTooltip({
|
|
420
|
-
index: currentIndex,
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
activeTooltipRequest = { index: currentIndex };
|
|
425
|
-
if (mode === 'split') {
|
|
426
|
-
showSplitTooltipAtIndex(currentIndex);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
showSharedTooltipAtIndex(currentIndex);
|
|
451
|
+
renderTooltip({ index: currentIndex });
|
|
430
452
|
})
|
|
431
453
|
.on('blur', function (event) {
|
|
432
454
|
select(this).attr('stroke', 'none').attr('stroke-width', 0);
|
|
@@ -448,12 +470,12 @@ export function attachXYTooltipArea(config) {
|
|
|
448
470
|
event.preventDefault();
|
|
449
471
|
focusTargetNodes[nextIndex].focus();
|
|
450
472
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
473
|
+
const detachScrollListeners = attachTooltipScrollListeners(tooltipSvgNode, updateTooltipPosition);
|
|
474
|
+
return () => {
|
|
475
|
+
detachScrollListeners?.();
|
|
476
|
+
queuedRender.cancel();
|
|
477
|
+
stopPositionTracking();
|
|
478
|
+
};
|
|
457
479
|
}
|
|
458
480
|
function normalizeFormatterValue(value) {
|
|
459
481
|
if (value === null ||
|
package/dist/tooltip.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type XYTooltipSeries } from './tooltip/types.js';
|
|
|
6
6
|
export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
7
7
|
private static nextTooltipId;
|
|
8
8
|
readonly id: string;
|
|
9
|
-
readonly type:
|
|
9
|
+
readonly type: 'tooltip';
|
|
10
10
|
readonly mode: TooltipMode;
|
|
11
11
|
readonly position: TooltipPosition;
|
|
12
12
|
readonly barAnchorPosition: TooltipBarAnchorPosition;
|
|
@@ -31,13 +31,21 @@ export declare class WordCloudChart extends BaseChart {
|
|
|
31
31
|
private hasRenderedLive;
|
|
32
32
|
private nextRenderShouldAnimate;
|
|
33
33
|
private previousWordSnapshot;
|
|
34
|
+
private resizeTimeout;
|
|
35
|
+
private pendingResize;
|
|
36
|
+
private resolvePendingResize;
|
|
34
37
|
constructor(config: WordCloudChartConfig);
|
|
35
38
|
update(data: ChartData): void;
|
|
36
39
|
destroy(): void;
|
|
40
|
+
whenReady(): Promise<void>;
|
|
41
|
+
protected handleResize(): void;
|
|
42
|
+
private cancelPendingResize;
|
|
37
43
|
protected validateSourceData(data: ChartData): void;
|
|
38
44
|
protected renderChart({ svg, plotArea }: BaseRenderContext): void;
|
|
39
45
|
protected createExportChart(): BaseChart;
|
|
40
46
|
private prepareAnimationForUpdate;
|
|
47
|
+
private minimumFontSize;
|
|
48
|
+
private fitWordSizes;
|
|
41
49
|
private startLayout;
|
|
42
50
|
private renderWords;
|
|
43
51
|
private shouldAnimateWords;
|
package/dist/word-cloud-chart.js
CHANGED
|
@@ -2,6 +2,7 @@ import cloud from 'd3-cloud';
|
|
|
2
2
|
import { scaleSqrt } from 'd3';
|
|
3
3
|
import { BaseChart, } from './base-chart.js';
|
|
4
4
|
import { isGroupedData } from './grouped-data.js';
|
|
5
|
+
import { measureTextWidth } from './utils.js';
|
|
5
6
|
import { normalizeRadialAnimationConfig, } from './radial-animation.js';
|
|
6
7
|
const DEFAULT_OPTIONS = {
|
|
7
8
|
maxWords: 75,
|
|
@@ -14,6 +15,10 @@ const DEFAULT_OPTIONS = {
|
|
|
14
15
|
spiral: 'archimedean',
|
|
15
16
|
};
|
|
16
17
|
const INITIAL_WORD_SCALE = 0.2;
|
|
18
|
+
const MAX_LAYOUT_ATTEMPTS = 6;
|
|
19
|
+
const LAYOUT_TIME_SLICE_MS = 8;
|
|
20
|
+
const FONT_SIZE_REDUCTION = 0.75;
|
|
21
|
+
const RESIZE_DEBOUNCE_MS = 150;
|
|
17
22
|
const GROUPED_DATA_ERROR = 'WordCloudChart: grouped datasets are not supported; provide a flat array of rows instead';
|
|
18
23
|
function createPreparedWords(data, plotArea, options, colors) {
|
|
19
24
|
const counts = new Map();
|
|
@@ -54,6 +59,18 @@ function createPreparedWords(data, plotArea, options, colors) {
|
|
|
54
59
|
};
|
|
55
60
|
});
|
|
56
61
|
}
|
|
62
|
+
function getPlacedPrefix(words, placedWords) {
|
|
63
|
+
const placedByText = new Map(placedWords.map((word) => [word.text, word]));
|
|
64
|
+
const prefix = [];
|
|
65
|
+
// A missing common word must never be replaced by rarer words.
|
|
66
|
+
for (const word of words) {
|
|
67
|
+
const placed = placedByText.get(word.text);
|
|
68
|
+
if (!placed)
|
|
69
|
+
break;
|
|
70
|
+
prefix.push(placed);
|
|
71
|
+
}
|
|
72
|
+
return prefix;
|
|
73
|
+
}
|
|
57
74
|
export class WordCloudChart extends BaseChart {
|
|
58
75
|
constructor(config) {
|
|
59
76
|
super(config);
|
|
@@ -105,6 +122,24 @@ export class WordCloudChart extends BaseChart {
|
|
|
105
122
|
writable: true,
|
|
106
123
|
value: new Map()
|
|
107
124
|
});
|
|
125
|
+
Object.defineProperty(this, "resizeTimeout", {
|
|
126
|
+
enumerable: true,
|
|
127
|
+
configurable: true,
|
|
128
|
+
writable: true,
|
|
129
|
+
value: null
|
|
130
|
+
});
|
|
131
|
+
Object.defineProperty(this, "pendingResize", {
|
|
132
|
+
enumerable: true,
|
|
133
|
+
configurable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
value: null
|
|
136
|
+
});
|
|
137
|
+
Object.defineProperty(this, "resolvePendingResize", {
|
|
138
|
+
enumerable: true,
|
|
139
|
+
configurable: true,
|
|
140
|
+
writable: true,
|
|
141
|
+
value: null
|
|
142
|
+
});
|
|
108
143
|
const wordCloud = config.wordCloud ?? {};
|
|
109
144
|
this.options = {
|
|
110
145
|
maxWords: wordCloud.maxWords ?? DEFAULT_OPTIONS.maxWords,
|
|
@@ -127,20 +162,62 @@ export class WordCloudChart extends BaseChart {
|
|
|
127
162
|
super.update(data);
|
|
128
163
|
}
|
|
129
164
|
destroy() {
|
|
165
|
+
this.cancelPendingResize();
|
|
130
166
|
this.layoutRunId += 1;
|
|
131
167
|
this.stopLayout();
|
|
132
168
|
this.setReadyPromise(Promise.resolve());
|
|
133
169
|
super.destroy();
|
|
134
170
|
}
|
|
171
|
+
whenReady() {
|
|
172
|
+
if (this.pendingResize) {
|
|
173
|
+
return this.pendingResize.then(() => super.whenReady());
|
|
174
|
+
}
|
|
175
|
+
return super.whenReady();
|
|
176
|
+
}
|
|
177
|
+
handleResize() {
|
|
178
|
+
if (!this.hasContainerSizeChanged()) {
|
|
179
|
+
this.cancelPendingResize();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (this.resizeTimeout !== null) {
|
|
183
|
+
clearTimeout(this.resizeTimeout);
|
|
184
|
+
}
|
|
185
|
+
// Keep the same promise throughout a burst of resize notifications.
|
|
186
|
+
if (!this.pendingResize) {
|
|
187
|
+
this.pendingResize = new Promise((resolve) => {
|
|
188
|
+
this.resolvePendingResize = resolve;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
this.resizeTimeout = setTimeout(() => {
|
|
192
|
+
this.resizeTimeout = null;
|
|
193
|
+
try {
|
|
194
|
+
super.handleResize();
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.cancelPendingResize();
|
|
198
|
+
}
|
|
199
|
+
}, RESIZE_DEBOUNCE_MS);
|
|
200
|
+
}
|
|
201
|
+
cancelPendingResize() {
|
|
202
|
+
if (this.resizeTimeout !== null) {
|
|
203
|
+
clearTimeout(this.resizeTimeout);
|
|
204
|
+
this.resizeTimeout = null;
|
|
205
|
+
}
|
|
206
|
+
this.resolvePendingResize?.();
|
|
207
|
+
this.resolvePendingResize = null;
|
|
208
|
+
this.pendingResize = null;
|
|
209
|
+
}
|
|
135
210
|
validateSourceData(data) {
|
|
136
211
|
if (isGroupedData(data)) {
|
|
137
212
|
throw new Error(GROUPED_DATA_ERROR);
|
|
138
213
|
}
|
|
139
214
|
}
|
|
140
215
|
renderChart({ svg, plotArea }) {
|
|
216
|
+
this.cancelPendingResize();
|
|
141
217
|
this.stopLayout();
|
|
142
218
|
this.renderTitle(svg);
|
|
143
|
-
const
|
|
219
|
+
const preparedWords = createPreparedWords(this.data, plotArea, this.options, this.renderTheme.colorPalette);
|
|
220
|
+
const words = this.fitWordSizes(preparedWords, plotArea, svg.node());
|
|
144
221
|
this.setReadyPromise(new Promise((resolve) => {
|
|
145
222
|
this.resolvePendingReady = resolve;
|
|
146
223
|
this.startLayout(words, plotArea, ++this.layoutRunId, resolve);
|
|
@@ -163,9 +240,46 @@ export class WordCloudChart extends BaseChart {
|
|
|
163
240
|
this.animation.duration > 0 &&
|
|
164
241
|
this.hasRenderedLive;
|
|
165
242
|
}
|
|
166
|
-
|
|
243
|
+
minimumFontSize(plotArea) {
|
|
244
|
+
return Math.max(1, Math.ceil((Math.min(plotArea.width, plotArea.height) *
|
|
245
|
+
this.options.minFontSize) /
|
|
246
|
+
100));
|
|
247
|
+
}
|
|
248
|
+
fitWordSizes(words, plotArea, svg) {
|
|
249
|
+
const minimumSize = this.minimumFontSize(plotArea);
|
|
250
|
+
let scale = 1;
|
|
251
|
+
words.forEach((word, index) => {
|
|
252
|
+
// d3-cloud measures at size + 1 and rounds sprite widths to 32px.
|
|
253
|
+
const width = measureTextWidth(word.text, word.size + 1, this.renderTheme.fontFamily, String(this.renderTheme.valueLabel.fontWeight), svg) + 1;
|
|
254
|
+
const height = (word.size + 1) * 2;
|
|
255
|
+
let rotatedWidth = width;
|
|
256
|
+
let rotatedHeight = height;
|
|
257
|
+
if (this.options.rotation === 'right-angle' && index % 2 === 1) {
|
|
258
|
+
rotatedWidth = height;
|
|
259
|
+
rotatedHeight = width;
|
|
260
|
+
}
|
|
261
|
+
else if (this.options.rotation === undefined) {
|
|
262
|
+
// Leave room for any angle selected by d3-cloud.
|
|
263
|
+
rotatedWidth = rotatedHeight = Math.hypot(width, height);
|
|
264
|
+
}
|
|
265
|
+
const padding = this.options.padding * 2;
|
|
266
|
+
const fit = Math.min(1, Math.max(1, plotArea.width - 32 - padding) / rotatedWidth, Math.max(1, plotArea.height - padding) / rotatedHeight);
|
|
267
|
+
const fittedSize = (word.size + 1) * fit - 1;
|
|
268
|
+
if (word.size > minimumSize) {
|
|
269
|
+
scale = Math.min(scale, Math.max(0, (fittedSize - minimumSize) / (word.size - minimumSize)));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// Compress the range uniformly to preserve frequency order and the
|
|
273
|
+
// configured minimum size, including for long, high-frequency phrases.
|
|
274
|
+
return words.map((word) => ({
|
|
275
|
+
...word,
|
|
276
|
+
size: Math.max(minimumSize, minimumSize + (word.size - minimumSize) * scale),
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
startLayout(words, plotArea, runId, resolve, attempt = 1, bestWords = []) {
|
|
167
280
|
const layout = cloud()
|
|
168
281
|
.words(words.map((word) => ({ ...word })))
|
|
282
|
+
.timeInterval(LAYOUT_TIME_SLICE_MS)
|
|
169
283
|
.size([
|
|
170
284
|
Math.max(1, Math.floor(plotArea.width)),
|
|
171
285
|
Math.max(1, Math.floor(plotArea.height)),
|
|
@@ -177,18 +291,39 @@ export class WordCloudChart extends BaseChart {
|
|
|
177
291
|
.fontSize((word) => word.size)
|
|
178
292
|
.text((word) => word.text)
|
|
179
293
|
.on('end', (placedWords) => {
|
|
180
|
-
this.layout = null;
|
|
181
294
|
if (runId !== this.layoutRunId ||
|
|
182
295
|
!this.plotGroup ||
|
|
183
296
|
!this.plotArea) {
|
|
184
297
|
this.finishReady(resolve);
|
|
185
298
|
return;
|
|
186
299
|
}
|
|
187
|
-
|
|
188
|
-
|
|
300
|
+
this.layout = null;
|
|
301
|
+
const prioritizedWords = getPlacedPrefix(words, placedWords);
|
|
302
|
+
if (prioritizedWords.length > bestWords.length) {
|
|
303
|
+
bestWords = prioritizedWords;
|
|
189
304
|
}
|
|
190
|
-
|
|
191
|
-
|
|
305
|
+
if (bestWords.length < words.length &&
|
|
306
|
+
attempt < MAX_LAYOUT_ATTEMPTS) {
|
|
307
|
+
const minimumSize = this.minimumFontSize(plotArea);
|
|
308
|
+
const smallerWords = words.map((word) => ({
|
|
309
|
+
...word,
|
|
310
|
+
// Always try the minimum on the final attempt.
|
|
311
|
+
size: attempt === MAX_LAYOUT_ATTEMPTS - 1
|
|
312
|
+
? minimumSize
|
|
313
|
+
: Math.max(minimumSize, word.size * FONT_SIZE_REDUCTION),
|
|
314
|
+
}));
|
|
315
|
+
const canShrink = smallerWords.some((word, index) => Math.floor(word.size) <
|
|
316
|
+
Math.floor(words[index].size));
|
|
317
|
+
if (canShrink) {
|
|
318
|
+
this.startLayout(smallerWords, plotArea, runId, resolve, attempt + 1, bestWords);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (bestWords.length < words.length) {
|
|
323
|
+
console.warn(`[Chart Warning] WordCloudChart: rendered ${bestWords.length} of ${words.length} words within the available area after ${attempt} layout attempt(s); retained the highest-frequency words`);
|
|
324
|
+
}
|
|
325
|
+
const transitions = this.renderWords(this.plotGroup, this.plotArea, bestWords);
|
|
326
|
+
this.completeRender(bestWords, transitions).then(() => {
|
|
192
327
|
this.finishReady(resolve);
|
|
193
328
|
});
|
|
194
329
|
});
|
|
@@ -395,7 +530,9 @@ export class WordCloudChart extends BaseChart {
|
|
|
395
530
|
this.resolvePendingReady = null;
|
|
396
531
|
}
|
|
397
532
|
finishReady(resolve) {
|
|
398
|
-
this.resolvePendingReady
|
|
533
|
+
if (this.resolvePendingReady === resolve) {
|
|
534
|
+
this.resolvePendingReady = null;
|
|
535
|
+
}
|
|
399
536
|
resolve();
|
|
400
537
|
}
|
|
401
538
|
}
|
package/dist/x-axis.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Selection } from 'd3';
|
|
|
2
2
|
import type { XAxisConfig, ChartTheme, D3Scale, DataItem, ExportHooks, XAxisConfigBase } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
4
|
export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
5
|
-
readonly type:
|
|
5
|
+
readonly type: 'xAxis';
|
|
6
6
|
readonly display: boolean;
|
|
7
7
|
readonly dataKey?: string;
|
|
8
8
|
readonly labelKey?: string;
|
package/dist/y-axis.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Selection } from 'd3';
|
|
|
2
2
|
import type { ChartTheme, YAxisConfig, D3Scale, ExportHooks, YAxisConfigBase } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
4
|
export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
|
|
5
|
-
readonly type:
|
|
5
|
+
readonly type: 'yAxis';
|
|
6
6
|
readonly display: boolean;
|
|
7
7
|
private readonly tickPadding;
|
|
8
8
|
private fontSize;
|
package/docs/components.md
CHANGED
|
@@ -167,6 +167,11 @@ paths are omitted when the arrow is already close to its target. For line,
|
|
|
167
167
|
area, and scatter points, side split tooltips spread nearby point targets
|
|
168
168
|
within the connectorless arrow range before drawing connector paths.
|
|
169
169
|
|
|
170
|
+
Visible XY tooltips track their targets on every animation frame, including
|
|
171
|
+
during touch and momentum scrolling. Their initial placement is preserved,
|
|
172
|
+
allowing them to pass beyond the container edges instead of sticking there
|
|
173
|
+
until blur. Tracking stops when the tooltips are hidden or the chart is destroyed.
|
|
174
|
+
|
|
170
175
|
For horizontal bar charts, prefer `position: 'auto'` or `position: 'vertical'`.
|
|
171
176
|
`position: 'side'` and `barAnchorPosition: 'top' | 'middle'` are kept for
|
|
172
177
|
legacy configs, but horizontal bars resolve bar anchoring automatically.
|
package/docs/word-cloud-chart.md
CHANGED
|
@@ -126,7 +126,23 @@ chart.render('#word-cloud');
|
|
|
126
126
|
## Notes
|
|
127
127
|
|
|
128
128
|
- `minFontSize` and `maxFontSize` use percentages of the smaller plot-area
|
|
129
|
-
dimension and define the
|
|
129
|
+
dimension and define the initial size range. The chart fits long phrases to
|
|
130
|
+
the available area and may reduce text sizes while preserving frequency order
|
|
131
|
+
and the configured minimum (rounded up to a whole pixel, at least one pixel).
|
|
132
|
+
- Placement makes at most six attempts, stopping earlier when all words fit or
|
|
133
|
+
text sizes cannot shrink further. If a sixth attempt is needed, it uses the minimum text size.
|
|
134
|
+
Each attempt yields between short batches of work. Updating, resizing, or
|
|
135
|
+
destroying the chart cancels pending placement.
|
|
136
|
+
- If words still do not fit, the chart retains the longest successfully placed
|
|
137
|
+
prefix of the frequency-sorted input across those attempts and logs a warning.
|
|
138
|
+
It never substitutes rarer words for a missing more frequent word. If even the
|
|
139
|
+
most frequent word cannot fit at the minimum size, the cloud may be empty.
|
|
140
|
+
- `whenReady()` resolves after the final placement and any animation, including
|
|
141
|
+
when only part of the input can be rendered.
|
|
142
|
+
- Container resizing is debounced by 150 ms. `whenReady()` also waits for a
|
|
143
|
+
pending resize and its layout. Initial renders and data updates remain
|
|
144
|
+
immediate. Updates, explicit renders, and destruction cancel queued resize
|
|
145
|
+
work; returning to the current rendered dimensions skips the rerender.
|
|
130
146
|
- The chart uses theme typography and palette colors directly when configuring
|
|
131
147
|
`d3-cloud` and rendering the final SVG.
|
|
132
148
|
- If `rotation` is omitted, the chart uses the native `d3-cloud` rotate
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.20.1",
|
|
3
3
|
"name": "@internetstiftelsen/charts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
@@ -50,56 +50,57 @@
|
|
|
50
50
|
"write-excel-file": "^4.1.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@chromatic-com/storybook": "^5.
|
|
53
|
+
"@chromatic-com/storybook": "^5.3.0",
|
|
54
54
|
"@eslint/js": "^10.0.1",
|
|
55
|
-
"@handsontable/react-wrapper": "^
|
|
55
|
+
"@handsontable/react-wrapper": "^18.0.0",
|
|
56
56
|
"@internetstiftelsen/styleguide": "^5.1.27",
|
|
57
|
-
"@radix-ui/react-label": "^2.1.
|
|
58
|
-
"@radix-ui/react-select": "^2.3.
|
|
59
|
-
"@radix-ui/react-switch": "^1.3.
|
|
60
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
61
|
-
"@speed-highlight/core": "^1.2.
|
|
62
|
-
"@storybook/addon-a11y": "^10.
|
|
63
|
-
"@storybook/addon-docs": "^10.
|
|
64
|
-
"@storybook/addon-mcp": "^0.
|
|
65
|
-
"@storybook/addon-vitest": "^10.
|
|
66
|
-
"@storybook/react-vite": "^10.
|
|
67
|
-
"@tailwindcss/vite": "^4.3.
|
|
57
|
+
"@radix-ui/react-label": "^2.1.15",
|
|
58
|
+
"@radix-ui/react-select": "^2.3.7",
|
|
59
|
+
"@radix-ui/react-switch": "^1.3.7",
|
|
60
|
+
"@radix-ui/react-tabs": "^1.1.21",
|
|
61
|
+
"@speed-highlight/core": "^1.2.24",
|
|
62
|
+
"@storybook/addon-a11y": "^10.5.7",
|
|
63
|
+
"@storybook/addon-docs": "^10.5.7",
|
|
64
|
+
"@storybook/addon-mcp": "^0.7.0",
|
|
65
|
+
"@storybook/addon-vitest": "^10.5.7",
|
|
66
|
+
"@storybook/react-vite": "^10.5.7",
|
|
67
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
68
68
|
"@testing-library/dom": "^10.4.1",
|
|
69
|
-
"@testing-library/jest-dom": "^
|
|
69
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
70
70
|
"@testing-library/react": "^16.3.2",
|
|
71
71
|
"@types/d3": "^7.4.3",
|
|
72
72
|
"@types/d3-cloud": "^1.2.9",
|
|
73
|
-
"@types/node": "^
|
|
74
|
-
"@types/react": "^19.2.
|
|
75
|
-
"@types/react-dom": "^19.2.
|
|
76
|
-
"@
|
|
77
|
-
"@
|
|
78
|
-
"@
|
|
73
|
+
"@types/node": "^26.2.0",
|
|
74
|
+
"@types/react": "^19.2.18",
|
|
75
|
+
"@types/react-dom": "^19.2.4",
|
|
76
|
+
"@typescript/native": "npm:typescript@~7.0.2",
|
|
77
|
+
"@vitejs/plugin-react": "^6.0.5",
|
|
78
|
+
"@vitest/browser-playwright": "4.1.10",
|
|
79
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
79
80
|
"class-variance-authority": "^0.7.1",
|
|
80
81
|
"clsx": "^2.1.1",
|
|
81
|
-
"eslint": "^10.
|
|
82
|
+
"eslint": "^10.8.1",
|
|
82
83
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
83
|
-
"eslint-plugin-react-refresh": "^0.5.
|
|
84
|
-
"eslint-plugin-storybook": "^10.
|
|
85
|
-
"globals": "^17.
|
|
86
|
-
"handsontable": "^
|
|
87
|
-
"jsdom": "^
|
|
88
|
-
"lucide-react": "^1.
|
|
89
|
-
"playwright": "^1.
|
|
90
|
-
"prettier": "3.
|
|
91
|
-
"radix-ui": "^1.
|
|
92
|
-
"react": "^19.2.
|
|
93
|
-
"react-dom": "^19.2.
|
|
94
|
-
"sass": "^1.
|
|
95
|
-
"storybook": "^10.
|
|
84
|
+
"eslint-plugin-react-refresh": "^0.5.3",
|
|
85
|
+
"eslint-plugin-storybook": "^10.5.7",
|
|
86
|
+
"globals": "^17.9.0",
|
|
87
|
+
"handsontable": "^18.0.0",
|
|
88
|
+
"jsdom": "^30.0.1",
|
|
89
|
+
"lucide-react": "^1.30.0",
|
|
90
|
+
"playwright": "^1.62.1",
|
|
91
|
+
"prettier": "3.9.6",
|
|
92
|
+
"radix-ui": "^1.6.7",
|
|
93
|
+
"react": "^19.2.8",
|
|
94
|
+
"react-dom": "^19.2.8",
|
|
95
|
+
"sass": "^1.102.0",
|
|
96
|
+
"storybook": "^10.5.7",
|
|
96
97
|
"tailwind-merge": "^3.6.0",
|
|
97
|
-
"tailwindcss": "^4.3.
|
|
98
|
-
"tsc-alias": "^1.
|
|
98
|
+
"tailwindcss": "^4.3.3",
|
|
99
|
+
"tsc-alias": "^1.9.1",
|
|
99
100
|
"tw-animate-css": "^1.4.0",
|
|
100
|
-
"typescript": "
|
|
101
|
-
"typescript-eslint": "^8.
|
|
102
|
-
"vite": "^8.
|
|
103
|
-
"vitest": "^4.1.
|
|
101
|
+
"typescript": "npm:@typescript/typescript6@~6.0.2",
|
|
102
|
+
"typescript-eslint": "^8.66.0",
|
|
103
|
+
"vite": "^8.2.1",
|
|
104
|
+
"vitest": "^4.1.10"
|
|
104
105
|
}
|
|
105
106
|
}
|