ui-svelte 0.2.10 → 0.2.12
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/dist/charts/ArcChart.svelte +9 -13
- package/dist/charts/ArcChart.svelte.d.ts +3 -3
- package/dist/charts/AreaChart.svelte +347 -118
- package/dist/charts/AreaChart.svelte.d.ts +33 -4
- package/dist/charts/BarChart.svelte +288 -66
- package/dist/charts/BarChart.svelte.d.ts +26 -1
- package/dist/charts/Candlestick.svelte +53 -50
- package/dist/charts/Candlestick.svelte.d.ts +8 -8
- package/dist/charts/LineChart.svelte +391 -91
- package/dist/charts/LineChart.svelte.d.ts +26 -3
- package/dist/charts/PieChart.svelte +333 -92
- package/dist/charts/PieChart.svelte.d.ts +33 -5
- package/dist/charts/css/arc-chart.css +3 -3
- package/dist/charts/css/area-chart.css +127 -29
- package/dist/charts/css/bar-chart.css +114 -8
- package/dist/charts/css/candlestick.css +2 -0
- package/dist/charts/css/line-chart.css +111 -13
- package/dist/charts/css/pie-chart.css +92 -20
- package/dist/control/Audio.svelte +86 -44
- package/dist/control/Audio.svelte.d.ts +4 -1
- package/dist/control/Button.svelte +18 -27
- package/dist/control/Button.svelte.d.ts +3 -2
- package/dist/control/IconButton.svelte +17 -27
- package/dist/control/IconButton.svelte.d.ts +3 -3
- package/dist/control/Image.svelte +123 -0
- package/dist/control/Image.svelte.d.ts +13 -0
- package/dist/control/Record.svelte +144 -98
- package/dist/control/Record.svelte.d.ts +2 -1
- package/dist/control/ToggleGroup.svelte +22 -8
- package/dist/control/ToggleGroup.svelte.d.ts +2 -1
- package/dist/control/ToggleTheme.svelte +13 -11
- package/dist/control/ToggleTheme.svelte.d.ts +3 -2
- package/dist/control/Video.svelte +55 -29
- package/dist/control/Video.svelte.d.ts +1 -0
- package/dist/control/css/btn.css +200 -152
- package/dist/control/css/image.css +56 -0
- package/dist/control/css/media.css +95 -30
- package/dist/control/css/toggle-group.css +269 -84
- package/dist/control/css/video.css +1 -14
- package/dist/css/animations.css +427 -2
- package/dist/css/base.css +13 -347
- package/dist/css/decorations.css +402 -0
- package/dist/css/rich-text.css +485 -0
- package/dist/css/transitions.css +158 -0
- package/dist/css/typography.css +291 -0
- package/dist/display/Accordion.svelte +28 -4
- package/dist/display/Accordion.svelte.d.ts +2 -1
- package/dist/display/Alert.svelte +32 -12
- package/dist/display/Alert.svelte.d.ts +2 -3
- package/dist/display/Avatar.svelte +54 -26
- package/dist/display/Avatar.svelte.d.ts +7 -1
- package/dist/display/AvatarGroup.svelte +26 -18
- package/dist/display/AvatarGroup.svelte.d.ts +9 -3
- package/dist/display/Badge.svelte +11 -4
- package/dist/display/Badge.svelte.d.ts +2 -1
- package/dist/display/Card.svelte +15 -14
- package/dist/display/Card.svelte.d.ts +2 -3
- package/dist/display/Carousel.svelte +130 -99
- package/dist/display/Carousel.svelte.d.ts +6 -4
- package/dist/display/ChatBox.svelte +245 -106
- package/dist/display/ChatBox.svelte.d.ts +32 -5
- package/dist/display/Chip.svelte +31 -17
- package/dist/display/Chip.svelte.d.ts +3 -2
- package/dist/display/Code.svelte +6 -3
- package/dist/display/Code.svelte.d.ts +1 -0
- package/dist/display/Collapsible.svelte +30 -4
- package/dist/display/Collapsible.svelte.d.ts +2 -1
- package/dist/display/Empty.svelte +37 -3
- package/dist/display/Empty.svelte.d.ts +3 -0
- package/dist/display/Item.svelte +31 -18
- package/dist/display/Item.svelte.d.ts +2 -2
- package/dist/display/Map.svelte +488 -0
- package/dist/display/Map.svelte.d.ts +44 -0
- package/dist/display/Section.svelte +14 -12
- package/dist/display/Section.svelte.d.ts +2 -3
- package/dist/display/Skeleton.svelte +32 -0
- package/dist/display/Skeleton.svelte.d.ts +10 -0
- package/dist/display/Table.svelte +94 -132
- package/dist/display/Table.svelte.d.ts +10 -1
- package/dist/display/css/accordion.css +349 -52
- package/dist/display/css/alert.css +18 -25
- package/dist/display/css/avatar-group.css +38 -44
- package/dist/display/css/avatar.css +152 -123
- package/dist/display/css/badge.css +50 -27
- package/dist/display/css/card.css +51 -71
- package/dist/display/css/carousel.css +25 -5
- package/dist/display/css/chat-box.css +158 -26
- package/dist/display/css/chip.css +142 -68
- package/dist/display/css/code.css +2 -6
- package/dist/display/css/collapsible.css +349 -45
- package/dist/display/css/divider.css +8 -6
- package/dist/display/css/empty.css +7 -0
- package/dist/display/css/item.css +311 -89
- package/dist/display/css/map.css +164 -0
- package/dist/display/css/section.css +78 -33
- package/dist/display/css/skeleton.css +58 -0
- package/dist/display/css/table.css +320 -189
- package/dist/form/Checkbox.svelte +11 -5
- package/dist/form/Checkbox.svelte.d.ts +2 -1
- package/dist/form/ColorField.svelte +543 -0
- package/dist/form/ColorField.svelte.d.ts +29 -0
- package/dist/form/ComboBox.svelte +24 -9
- package/dist/form/ComboBox.svelte.d.ts +2 -2
- package/dist/form/CsvField.svelte +62 -136
- package/dist/form/CsvField.svelte.d.ts +2 -2
- package/dist/form/DateField.svelte +33 -15
- package/dist/form/DateField.svelte.d.ts +2 -1
- package/dist/form/DateRange.svelte +436 -0
- package/dist/form/DateRange.svelte.d.ts +24 -0
- package/dist/form/DragDrop.svelte +348 -0
- package/dist/form/DragDrop.svelte.d.ts +32 -0
- package/dist/form/Dropzone.svelte +28 -8
- package/dist/form/Dropzone.svelte.d.ts +2 -2
- package/dist/form/Editor.svelte +626 -0
- package/dist/form/Editor.svelte.d.ts +50 -0
- package/dist/form/ImageCropper.svelte +291 -61
- package/dist/form/ImageCropper.svelte.d.ts +15 -1
- package/dist/form/PasswordField.svelte +120 -75
- package/dist/form/PasswordField.svelte.d.ts +9 -10
- package/dist/form/PhoneField.svelte +34 -16
- package/dist/form/PhoneField.svelte.d.ts +4 -3
- package/dist/form/PinField.svelte +39 -31
- package/dist/form/PinField.svelte.d.ts +3 -3
- package/dist/form/RadioGroup.svelte +4 -4
- package/dist/form/RadioGroup.svelte.d.ts +1 -1
- package/dist/form/Select.svelte +20 -19
- package/dist/form/Select.svelte.d.ts +2 -2
- package/dist/form/Slider.svelte +4 -2
- package/dist/form/Slider.svelte.d.ts +1 -0
- package/dist/form/TextField.svelte +16 -7
- package/dist/form/TextField.svelte.d.ts +2 -2
- package/dist/form/Textarea.svelte +15 -6
- package/dist/form/Textarea.svelte.d.ts +2 -2
- package/dist/form/Toggle.svelte +11 -1
- package/dist/form/Toggle.svelte.d.ts +2 -0
- package/dist/form/css/checkbox.css +18 -2
- package/dist/form/css/color-field.css +141 -0
- package/dist/form/css/control.css +193 -82
- package/dist/form/css/csv-field.css +226 -0
- package/dist/form/css/date-range.css +122 -0
- package/dist/form/css/date.css +24 -2
- package/dist/form/css/drag-drop.css +271 -0
- package/dist/form/css/dropzone.css +153 -34
- package/dist/form/css/editor.css +367 -0
- package/dist/form/css/field.css +4 -0
- package/dist/form/css/image-cropper.css +223 -22
- package/dist/form/css/password.css +1 -1
- package/dist/form/css/radio-group.css +1 -1
- package/dist/form/css/select.css +2 -2
- package/dist/form/css/slider.css +1 -0
- package/dist/form/css/textarea.css +178 -75
- package/dist/form/css/toggle.css +11 -2
- package/dist/hooks/use-table.svelte.d.ts +1 -0
- package/dist/hooks/use-table.svelte.js +6 -0
- package/dist/icons/index.d.ts +38 -2
- package/dist/icons/index.js +40 -4
- package/dist/index.css +16 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +10 -2
- package/dist/layout/AppBar.svelte +22 -14
- package/dist/layout/AppBar.svelte.d.ts +2 -1
- package/dist/layout/Footer.svelte +19 -11
- package/dist/layout/Footer.svelte.d.ts +2 -1
- package/dist/layout/Provider.svelte +27 -4
- package/dist/layout/Provider.svelte.d.ts +3 -1
- package/dist/layout/css/app-bar.css +63 -66
- package/dist/layout/css/footer.css +62 -65
- package/dist/navigation/BottomNav.svelte +41 -13
- package/dist/navigation/FooterGroup.svelte +1 -1
- package/dist/navigation/NavMenu.svelte +47 -23
- package/dist/navigation/NavMenu.svelte.d.ts +29 -0
- package/dist/navigation/Pagination.svelte +158 -0
- package/dist/navigation/Pagination.svelte.d.ts +18 -0
- package/dist/navigation/SideNav.svelte +30 -25
- package/dist/navigation/SideNav.svelte.d.ts +2 -3
- package/dist/navigation/Tabs.svelte +17 -7
- package/dist/navigation/Tabs.svelte.d.ts +2 -2
- package/dist/navigation/css/bottom-nav.css +279 -257
- package/dist/navigation/css/footer-group.css +1 -1
- package/dist/navigation/css/footer-nav.css +1 -1
- package/dist/navigation/css/nav-menu.css +332 -106
- package/dist/navigation/css/pagination.css +74 -0
- package/dist/navigation/css/side-nav.css +515 -75
- package/dist/navigation/css/tabs.css +246 -52
- package/dist/overlay/Command.svelte +340 -0
- package/dist/overlay/Command.svelte.d.ts +24 -25
- package/dist/overlay/Drawer.svelte +49 -21
- package/dist/overlay/Drawer.svelte.d.ts +2 -2
- package/dist/overlay/Dropdown.svelte +4 -5
- package/dist/overlay/Modal.svelte +51 -16
- package/dist/overlay/Modal.svelte.d.ts +3 -3
- package/dist/overlay/Toast.svelte +41 -17
- package/dist/overlay/Toast.svelte.d.ts +1 -1
- package/dist/overlay/Tooltip.svelte +40 -26
- package/dist/overlay/Tooltip.svelte.d.ts +2 -2
- package/dist/overlay/css/command.css +80 -0
- package/dist/overlay/css/drawer.css +63 -24
- package/dist/overlay/css/dropdown.css +1 -1
- package/dist/overlay/css/hovercard.css +1 -1
- package/dist/overlay/css/modal.css +27 -27
- package/dist/overlay/css/toast.css +17 -29
- package/dist/overlay/css/tooltip.css +83 -66
- package/dist/stores/theme.svelte.js +26 -1
- package/dist/stores/toast.svelte.d.ts +4 -4
- package/dist/stores/toast.svelte.js +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../utils/class-names.js';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
3
|
+
import { onMount, untrack, type Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
6
|
+
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
+
type LegendPosition = 'top' | 'right' | 'bottom' | 'left' | 'none';
|
|
8
|
+
type Palette = 'default' | 'rainbow' | 'ocean' | 'sunset' | 'forest' | 'neon';
|
|
4
9
|
|
|
5
10
|
type TooltipData = {
|
|
6
11
|
x: number;
|
|
@@ -16,8 +21,6 @@
|
|
|
16
21
|
range: [number, number];
|
|
17
22
|
};
|
|
18
23
|
|
|
19
|
-
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
20
|
-
|
|
21
24
|
type DataPoint = {
|
|
22
25
|
x: number;
|
|
23
26
|
y: number;
|
|
@@ -42,9 +45,11 @@
|
|
|
42
45
|
margin?: Margin;
|
|
43
46
|
color?: Color;
|
|
44
47
|
colors?: Color[];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
hidePoints?: boolean;
|
|
49
|
+
hideGrid?: boolean;
|
|
50
|
+
hideLegend?: boolean;
|
|
51
|
+
hideXAxis?: boolean;
|
|
52
|
+
hideYAxis?: boolean;
|
|
48
53
|
curve?: 'linear' | 'smooth';
|
|
49
54
|
strokeWidth?: number;
|
|
50
55
|
pointRadius?: number;
|
|
@@ -52,6 +57,19 @@
|
|
|
52
57
|
empty?: boolean;
|
|
53
58
|
emptyText?: string;
|
|
54
59
|
rootClass?: string;
|
|
60
|
+
chartClass?: string;
|
|
61
|
+
size?: Size;
|
|
62
|
+
palette?: Palette;
|
|
63
|
+
legendPosition?: LegendPosition;
|
|
64
|
+
disableAnimation?: boolean;
|
|
65
|
+
animationDuration?: number;
|
|
66
|
+
valueFormatter?: (value: number) => string;
|
|
67
|
+
xFormatter?: (value: number) => string;
|
|
68
|
+
onClick?: (point: DataPoint, seriesName: string, index: number) => void;
|
|
69
|
+
onHover?: (point: DataPoint | null, seriesName: string, index: number) => void;
|
|
70
|
+
tooltipContent?: Snippet<[{ point: DataPoint; seriesName: string; color: Color }]>;
|
|
71
|
+
showGradientFill?: boolean;
|
|
72
|
+
showGlow?: boolean;
|
|
55
73
|
};
|
|
56
74
|
|
|
57
75
|
let {
|
|
@@ -60,23 +78,99 @@
|
|
|
60
78
|
margin = { top: 20, right: 20, bottom: 40, left: 50 },
|
|
61
79
|
color = 'primary' as Color,
|
|
62
80
|
colors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger'] as Color[],
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
hidePoints = false,
|
|
82
|
+
hideGrid = false,
|
|
83
|
+
hideLegend = false,
|
|
84
|
+
hideXAxis = false,
|
|
85
|
+
hideYAxis = false,
|
|
66
86
|
curve = 'linear' as 'linear' | 'smooth',
|
|
67
87
|
strokeWidth = 2,
|
|
68
88
|
pointRadius = 3,
|
|
69
89
|
loading = false,
|
|
70
90
|
empty = false,
|
|
71
91
|
emptyText = 'No data available',
|
|
72
|
-
rootClass
|
|
92
|
+
rootClass,
|
|
93
|
+
chartClass,
|
|
94
|
+
size = 'md',
|
|
95
|
+
palette,
|
|
96
|
+
legendPosition = 'bottom',
|
|
97
|
+
disableAnimation = false,
|
|
98
|
+
animationDuration = 800,
|
|
99
|
+
valueFormatter,
|
|
100
|
+
xFormatter,
|
|
101
|
+
onClick,
|
|
102
|
+
onHover,
|
|
103
|
+
tooltipContent,
|
|
104
|
+
showGradientFill = false,
|
|
105
|
+
showGlow = false
|
|
73
106
|
}: Props = $props();
|
|
74
107
|
|
|
108
|
+
const sizePresets: Record<
|
|
109
|
+
Size,
|
|
110
|
+
{ height: number; strokeWidth: number; pointRadius: number; fontSize: number }
|
|
111
|
+
> = {
|
|
112
|
+
sm: { height: 200, strokeWidth: 1.5, pointRadius: 2, fontSize: 10 },
|
|
113
|
+
md: { height: 300, strokeWidth: 2, pointRadius: 3, fontSize: 12 },
|
|
114
|
+
lg: { height: 400, strokeWidth: 2.5, pointRadius: 4, fontSize: 14 },
|
|
115
|
+
xl: { height: 500, strokeWidth: 3, pointRadius: 5, fontSize: 16 }
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const colorPalettes: Record<Palette, Color[]> = {
|
|
119
|
+
default: ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'muted'],
|
|
120
|
+
rainbow: ['danger', 'warning', 'success', 'info', 'primary', 'secondary', 'muted'],
|
|
121
|
+
ocean: ['info', 'primary', 'secondary', 'success', 'muted', 'warning', 'danger'],
|
|
122
|
+
sunset: ['warning', 'danger', 'secondary', 'primary', 'info', 'success', 'muted'],
|
|
123
|
+
forest: ['success', 'primary', 'info', 'secondary', 'muted', 'warning', 'danger'],
|
|
124
|
+
neon: ['secondary', 'primary', 'success', 'warning', 'danger', 'info', 'muted']
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const colorClass: Record<Color, string> = {
|
|
128
|
+
primary: 'is-primary',
|
|
129
|
+
secondary: 'is-secondary',
|
|
130
|
+
success: 'is-success',
|
|
131
|
+
info: 'is-info',
|
|
132
|
+
warning: 'is-warning',
|
|
133
|
+
danger: 'is-danger',
|
|
134
|
+
muted: 'is-muted'
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function getSeriesColor(s: Series, index: number): Color {
|
|
138
|
+
if (s.color) return s.color;
|
|
139
|
+
if (palette) {
|
|
140
|
+
const paletteColors = colorPalettes[palette];
|
|
141
|
+
return paletteColors[index % paletteColors.length];
|
|
142
|
+
}
|
|
143
|
+
return colors[index % colors.length];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatValue(value: number): string {
|
|
147
|
+
if (valueFormatter) return valueFormatter(value);
|
|
148
|
+
if (value === 0) return '0';
|
|
149
|
+
const abs = Math.abs(value);
|
|
150
|
+
if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
|
|
151
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
152
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
153
|
+
if (abs < 1) return value.toFixed(2);
|
|
154
|
+
return value.toFixed(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatXValue(value: number): string {
|
|
158
|
+
if (xFormatter) return xFormatter(value);
|
|
159
|
+
return formatValue(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
75
162
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
76
163
|
let containerSize = $state({ width: 0, height: 0 });
|
|
77
164
|
|
|
165
|
+
let sizeConfig = $derived(sizePresets[size]);
|
|
166
|
+
let effectiveStrokeWidth = $derived(strokeWidth || sizeConfig.strokeWidth);
|
|
167
|
+
let effectivePointRadius = $derived(pointRadius || sizeConfig.pointRadius);
|
|
168
|
+
|
|
78
169
|
let width = $derived(containerSize.width || 600);
|
|
79
|
-
let height = $derived(containerSize.height ||
|
|
170
|
+
let height = $derived(containerSize.height || sizeConfig.height);
|
|
171
|
+
|
|
172
|
+
let animationProgress = $state(0);
|
|
173
|
+
let animationFrameId: number | null = null;
|
|
80
174
|
|
|
81
175
|
function createLinearScale(domain: [number, number], range: [number, number]): LinearScale {
|
|
82
176
|
const [d0, d1] = domain;
|
|
@@ -96,16 +190,6 @@
|
|
|
96
190
|
return scale;
|
|
97
191
|
}
|
|
98
192
|
|
|
99
|
-
function formatNumber(value: number): string {
|
|
100
|
-
if (value === 0) return '0';
|
|
101
|
-
const abs = Math.abs(value);
|
|
102
|
-
if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
|
|
103
|
-
if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
104
|
-
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
105
|
-
if (abs < 1) return value.toFixed(2);
|
|
106
|
-
return value.toFixed(0);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
193
|
let innerWidth = $derived(width - margin.left - margin.right);
|
|
110
194
|
let innerHeight = $derived(height - margin.top - margin.bottom);
|
|
111
195
|
|
|
@@ -114,7 +198,7 @@
|
|
|
114
198
|
return series.map((s, i) => ({
|
|
115
199
|
name: s.name,
|
|
116
200
|
data: s.data,
|
|
117
|
-
color: s
|
|
201
|
+
color: getSeriesColor(s, i)
|
|
118
202
|
}));
|
|
119
203
|
}
|
|
120
204
|
|
|
@@ -147,32 +231,76 @@
|
|
|
147
231
|
return [min - padding, max + padding];
|
|
148
232
|
});
|
|
149
233
|
|
|
150
|
-
function createPath(points: DataPoint[], smooth: boolean = false): string {
|
|
234
|
+
function createPath(points: DataPoint[], smooth: boolean = false, progress: number = 1): string {
|
|
151
235
|
if (points.length === 0) return '';
|
|
152
236
|
|
|
153
237
|
const xScale = createLinearScale(xDomain, [0, innerWidth]);
|
|
154
238
|
const yScale = createLinearScale(yDomain, [innerHeight, 0]);
|
|
155
239
|
|
|
156
|
-
|
|
240
|
+
const visiblePoints = Math.ceil(points.length * progress);
|
|
241
|
+
const displayPoints = points.slice(0, visiblePoints);
|
|
157
242
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
243
|
+
if (displayPoints.length === 0) return '';
|
|
244
|
+
|
|
245
|
+
let path = `M ${xScale(displayPoints[0].x)} ${yScale(displayPoints[0].y)}`;
|
|
246
|
+
|
|
247
|
+
if (smooth && displayPoints.length > 1) {
|
|
248
|
+
for (let i = 1; i < displayPoints.length; i++) {
|
|
249
|
+
const x0 = xScale(displayPoints[i - 1].x);
|
|
250
|
+
const y0 = yScale(displayPoints[i - 1].y);
|
|
251
|
+
const x1 = xScale(displayPoints[i].x);
|
|
252
|
+
const y1 = yScale(displayPoints[i].y);
|
|
164
253
|
const cpx = (x0 + x1) / 2;
|
|
165
254
|
path += ` C ${cpx} ${y0}, ${cpx} ${y1}, ${x1} ${y1}`;
|
|
166
255
|
}
|
|
167
256
|
} else {
|
|
168
|
-
for (let i = 1; i <
|
|
169
|
-
path += ` L ${xScale(
|
|
257
|
+
for (let i = 1; i < displayPoints.length; i++) {
|
|
258
|
+
path += ` L ${xScale(displayPoints[i].x)} ${yScale(displayPoints[i].y)}`;
|
|
170
259
|
}
|
|
171
260
|
}
|
|
172
261
|
|
|
173
262
|
return path;
|
|
174
263
|
}
|
|
175
264
|
|
|
265
|
+
function createAreaPath(
|
|
266
|
+
points: DataPoint[],
|
|
267
|
+
smooth: boolean = false,
|
|
268
|
+
progress: number = 1
|
|
269
|
+
): string {
|
|
270
|
+
if (points.length === 0) return '';
|
|
271
|
+
|
|
272
|
+
const xScale = createLinearScale(xDomain, [0, innerWidth]);
|
|
273
|
+
const yScale = createLinearScale(yDomain, [innerHeight, 0]);
|
|
274
|
+
|
|
275
|
+
const visiblePoints = Math.ceil(points.length * progress);
|
|
276
|
+
const displayPoints = points.slice(0, visiblePoints);
|
|
277
|
+
|
|
278
|
+
if (displayPoints.length === 0) return '';
|
|
279
|
+
|
|
280
|
+
let path = `M ${xScale(displayPoints[0].x)} ${innerHeight}`;
|
|
281
|
+
path += ` L ${xScale(displayPoints[0].x)} ${yScale(displayPoints[0].y)}`;
|
|
282
|
+
|
|
283
|
+
if (smooth && displayPoints.length > 1) {
|
|
284
|
+
for (let i = 1; i < displayPoints.length; i++) {
|
|
285
|
+
const x0 = xScale(displayPoints[i - 1].x);
|
|
286
|
+
const y0 = yScale(displayPoints[i - 1].y);
|
|
287
|
+
const x1 = xScale(displayPoints[i].x);
|
|
288
|
+
const y1 = yScale(displayPoints[i].y);
|
|
289
|
+
const cpx = (x0 + x1) / 2;
|
|
290
|
+
path += ` C ${cpx} ${y0}, ${cpx} ${y1}, ${x1} ${y1}`;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
for (let i = 1; i < displayPoints.length; i++) {
|
|
294
|
+
path += ` L ${xScale(displayPoints[i].x)} ${yScale(displayPoints[i].y)}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
path += ` L ${xScale(displayPoints[displayPoints.length - 1].x)} ${innerHeight}`;
|
|
299
|
+
path += ` Z`;
|
|
300
|
+
|
|
301
|
+
return path;
|
|
302
|
+
}
|
|
303
|
+
|
|
176
304
|
function createGridLines(): Array<{ x: number; y: number; value: number }> {
|
|
177
305
|
const yScale = createLinearScale(yDomain, [innerHeight, 0]);
|
|
178
306
|
const yTicks = 5;
|
|
@@ -183,29 +311,60 @@
|
|
|
183
311
|
});
|
|
184
312
|
}
|
|
185
313
|
|
|
314
|
+
function createXAxisTicks(): Array<{ x: number; value: number }> {
|
|
315
|
+
const xScale = createLinearScale(xDomain, [0, innerWidth]);
|
|
316
|
+
const xTicks = Math.min(6, Math.max(2, Math.floor(innerWidth / 80)));
|
|
317
|
+
|
|
318
|
+
return Array.from({ length: xTicks + 1 }, (_, i) => {
|
|
319
|
+
const value = xDomain[0] + (xDomain[1] - xDomain[0]) * (i / xTicks);
|
|
320
|
+
return { x: xScale(value), value };
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let gradientIds = $derived(
|
|
325
|
+
normalizedSeries.map((_, i) => `line-gradient-${i}-${Math.random().toString(36).slice(2, 9)}`)
|
|
326
|
+
);
|
|
327
|
+
|
|
186
328
|
let tooltipData = $state<TooltipData | null>(null);
|
|
187
329
|
let tooltipPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
188
330
|
let isTooltipActive = $state(false);
|
|
331
|
+
let hoveredSeriesIndex = $state<number | null>(null);
|
|
332
|
+
let hoveredPointIndex = $state<number | null>(null);
|
|
189
333
|
|
|
190
334
|
function handlePointHover(
|
|
191
335
|
point: DataPoint,
|
|
192
336
|
seriesName: string,
|
|
193
337
|
event: MouseEvent,
|
|
194
|
-
pointColor: Color
|
|
338
|
+
pointColor: Color,
|
|
339
|
+
seriesIndex: number,
|
|
340
|
+
pointIndex: number
|
|
195
341
|
): void {
|
|
196
342
|
const target = event.target as SVGCircleElement;
|
|
197
343
|
const rect = target.getBoundingClientRect();
|
|
198
344
|
|
|
199
345
|
tooltipData = { x: point.x, y: point.y, seriesName, color: pointColor };
|
|
200
346
|
tooltipPosition = {
|
|
201
|
-
x: rect.
|
|
202
|
-
y: rect.top
|
|
347
|
+
x: rect.left + rect.width / 2,
|
|
348
|
+
y: rect.top - 10
|
|
203
349
|
};
|
|
204
350
|
isTooltipActive = true;
|
|
351
|
+
hoveredSeriesIndex = seriesIndex;
|
|
352
|
+
hoveredPointIndex = pointIndex;
|
|
353
|
+
|
|
354
|
+
if (onHover) {
|
|
355
|
+
onHover(point, seriesName, pointIndex);
|
|
356
|
+
}
|
|
205
357
|
}
|
|
206
358
|
|
|
207
359
|
function handlePointLeave(): void {
|
|
208
360
|
isTooltipActive = false;
|
|
361
|
+
hoveredSeriesIndex = null;
|
|
362
|
+
hoveredPointIndex = null;
|
|
363
|
+
|
|
364
|
+
if (onHover) {
|
|
365
|
+
onHover(null, '', -1);
|
|
366
|
+
}
|
|
367
|
+
|
|
209
368
|
setTimeout(() => {
|
|
210
369
|
if (!isTooltipActive) {
|
|
211
370
|
tooltipData = null;
|
|
@@ -213,6 +372,36 @@
|
|
|
213
372
|
}, 100);
|
|
214
373
|
}
|
|
215
374
|
|
|
375
|
+
function handlePointClick(point: DataPoint, seriesName: string, index: number): void {
|
|
376
|
+
if (onClick) {
|
|
377
|
+
onClick(point, seriesName, index);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let containerLayoutClass = $derived(() => {
|
|
382
|
+
switch (legendPosition) {
|
|
383
|
+
case 'top':
|
|
384
|
+
return 'flex-col-reverse';
|
|
385
|
+
case 'bottom':
|
|
386
|
+
return 'flex-col';
|
|
387
|
+
case 'left':
|
|
388
|
+
return 'flex-row-reverse';
|
|
389
|
+
case 'right':
|
|
390
|
+
default:
|
|
391
|
+
return 'flex-row';
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
let legendLayoutClass = $derived(() => {
|
|
396
|
+
switch (legendPosition) {
|
|
397
|
+
case 'top':
|
|
398
|
+
case 'bottom':
|
|
399
|
+
return 'flex-row flex-wrap';
|
|
400
|
+
default:
|
|
401
|
+
return 'flex-col';
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
216
405
|
onMount(() => {
|
|
217
406
|
const updateSize = () => {
|
|
218
407
|
if (containerEl) {
|
|
@@ -221,19 +410,66 @@
|
|
|
221
410
|
}
|
|
222
411
|
};
|
|
223
412
|
|
|
413
|
+
const handleScroll = () => {
|
|
414
|
+
if (isTooltipActive) {
|
|
415
|
+
isTooltipActive = false;
|
|
416
|
+
hoveredSeriesIndex = null;
|
|
417
|
+
hoveredPointIndex = null;
|
|
418
|
+
tooltipData = null;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
224
422
|
updateSize();
|
|
225
423
|
const resizeObserver = new ResizeObserver(updateSize);
|
|
226
424
|
if (containerEl) {
|
|
227
425
|
resizeObserver.observe(containerEl);
|
|
228
426
|
}
|
|
229
427
|
|
|
428
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
429
|
+
|
|
230
430
|
return () => {
|
|
431
|
+
if (animationFrameId !== null) {
|
|
432
|
+
cancelAnimationFrame(animationFrameId);
|
|
433
|
+
}
|
|
231
434
|
resizeObserver.disconnect();
|
|
435
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
232
436
|
};
|
|
233
437
|
});
|
|
438
|
+
|
|
439
|
+
$effect(() => {
|
|
440
|
+
if (normalizedSeries.length === 0) return;
|
|
441
|
+
|
|
442
|
+
if (animationFrameId !== null) {
|
|
443
|
+
cancelAnimationFrame(animationFrameId);
|
|
444
|
+
animationFrameId = null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!disableAnimation) {
|
|
448
|
+
const startTime = Date.now();
|
|
449
|
+
const startProgress = untrack(() => animationProgress);
|
|
450
|
+
|
|
451
|
+
const animate = () => {
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
const progress = Math.min((now - startTime) / animationDuration, 1);
|
|
454
|
+
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
|
455
|
+
|
|
456
|
+
animationProgress = startProgress + (1 - startProgress) * easeProgress;
|
|
457
|
+
|
|
458
|
+
if (progress < 1) {
|
|
459
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
460
|
+
} else {
|
|
461
|
+
animationFrameId = null;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
466
|
+
} else {
|
|
467
|
+
animationProgress = 1;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
234
470
|
</script>
|
|
235
471
|
|
|
236
|
-
<div
|
|
472
|
+
<div class={cn('line-chart-container', `is-${size}`, containerLayoutClass(), rootClass)}>
|
|
237
473
|
{#if loading}
|
|
238
474
|
<div class="line-chart-loading">
|
|
239
475
|
<svg class="line-chart-loading-spinner" viewBox="0 0 24 24">
|
|
@@ -265,15 +501,28 @@
|
|
|
265
501
|
<span>{emptyText}</span>
|
|
266
502
|
</div>
|
|
267
503
|
{:else}
|
|
268
|
-
<div class=
|
|
504
|
+
<div bind:this={containerEl} class={cn('line-chart', `is-${size}`, chartClass)}>
|
|
269
505
|
<svg class="line-chart-svg" {width} {height}>
|
|
506
|
+
{#if showGradientFill}
|
|
507
|
+
<defs>
|
|
508
|
+
{#each normalizedSeries as s, i}
|
|
509
|
+
{@const seriesColor = getSeriesColor(s, i)}
|
|
510
|
+
<linearGradient id={gradientIds[i]} x1="0%" y1="0%" x2="0%" y2="100%">
|
|
511
|
+
<stop offset="0%" style="stop-color:var(--color-{seriesColor}); stop-opacity:0.3" />
|
|
512
|
+
<stop offset="100%" style="stop-color:var(--color-{seriesColor}); stop-opacity:0" />
|
|
513
|
+
</linearGradient>
|
|
514
|
+
{/each}
|
|
515
|
+
</defs>
|
|
516
|
+
{/if}
|
|
517
|
+
|
|
270
518
|
<g transform="translate({margin.left}, {margin.top})">
|
|
271
519
|
{#if normalizedSeries.length > 0}
|
|
272
520
|
{@const grid = createGridLines()}
|
|
521
|
+
{@const xTicks = createXAxisTicks()}
|
|
273
522
|
{@const xScale = createLinearScale(xDomain, [0, innerWidth])}
|
|
274
523
|
{@const yScale = createLinearScale(yDomain, [innerHeight, 0])}
|
|
275
524
|
|
|
276
|
-
{#if
|
|
525
|
+
{#if !hideGrid}
|
|
277
526
|
<g class="line-chart-grid">
|
|
278
527
|
{#each grid as line}
|
|
279
528
|
<line x1={0} y1={line.y} x2={line.x} y2={line.y} class="line-chart-grid-line" />
|
|
@@ -282,46 +531,90 @@
|
|
|
282
531
|
{/if}
|
|
283
532
|
|
|
284
533
|
<g class="line-chart-axis">
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{/
|
|
534
|
+
{#if !hideXAxis}
|
|
535
|
+
<line
|
|
536
|
+
x1={0}
|
|
537
|
+
y1={innerHeight}
|
|
538
|
+
x2={innerWidth}
|
|
539
|
+
y2={innerHeight}
|
|
540
|
+
class="line-chart-axis-line"
|
|
541
|
+
/>
|
|
542
|
+
{#each xTicks as tick}
|
|
543
|
+
<text
|
|
544
|
+
x={tick.x}
|
|
545
|
+
y={innerHeight + 20}
|
|
546
|
+
class="line-chart-axis-label"
|
|
547
|
+
text-anchor="middle"
|
|
548
|
+
style="font-size: {sizeConfig.fontSize}px;"
|
|
549
|
+
>
|
|
550
|
+
{formatXValue(tick.value)}
|
|
551
|
+
</text>
|
|
552
|
+
{/each}
|
|
553
|
+
{/if}
|
|
554
|
+
|
|
555
|
+
{#if !hideYAxis}
|
|
556
|
+
<line x1={0} y1={0} x2={0} y2={innerHeight} class="line-chart-axis-line" />
|
|
557
|
+
{#each grid as line}
|
|
558
|
+
<text
|
|
559
|
+
x={-10}
|
|
560
|
+
y={line.y}
|
|
561
|
+
class="line-chart-axis-label"
|
|
562
|
+
text-anchor="end"
|
|
563
|
+
dominant-baseline="middle"
|
|
564
|
+
style="font-size: {sizeConfig.fontSize}px;"
|
|
565
|
+
>
|
|
566
|
+
{formatValue(line.value)}
|
|
567
|
+
</text>
|
|
568
|
+
{/each}
|
|
569
|
+
{/if}
|
|
305
570
|
</g>
|
|
306
571
|
|
|
307
|
-
{#each normalizedSeries as s}
|
|
572
|
+
{#each normalizedSeries as s, i}
|
|
573
|
+
{@const seriesColor = getSeriesColor(s, i)}
|
|
574
|
+
{@const isHovered = hoveredSeriesIndex === i}
|
|
575
|
+
|
|
576
|
+
{#if showGradientFill}
|
|
577
|
+
<path
|
|
578
|
+
d={createAreaPath(s.data, curve === 'smooth', animationProgress)}
|
|
579
|
+
class="line-chart-area"
|
|
580
|
+
fill="url(#{gradientIds[i]})"
|
|
581
|
+
/>
|
|
582
|
+
{/if}
|
|
583
|
+
|
|
308
584
|
<path
|
|
309
|
-
d={createPath(s.data, curve === 'smooth')}
|
|
310
|
-
class=
|
|
311
|
-
|
|
585
|
+
d={createPath(s.data, curve === 'smooth', animationProgress)}
|
|
586
|
+
class={cn(
|
|
587
|
+
'line-chart-line',
|
|
588
|
+
colorClass[seriesColor],
|
|
589
|
+
showGlow && 'has-glow',
|
|
590
|
+
isHovered && 'is-hovered'
|
|
591
|
+
)}
|
|
592
|
+
style="stroke-width: {effectiveStrokeWidth};"
|
|
312
593
|
/>
|
|
313
594
|
|
|
314
|
-
{#if
|
|
315
|
-
{#each s.data as point}
|
|
316
|
-
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
595
|
+
{#if !hidePoints}
|
|
596
|
+
{#each s.data as point, pi}
|
|
597
|
+
{@const visibleIndex = Math.ceil(s.data.length * animationProgress)}
|
|
598
|
+
{#if pi < visibleIndex}
|
|
599
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
600
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
601
|
+
<circle
|
|
602
|
+
cx={xScale(point.x)}
|
|
603
|
+
cy={yScale(point.y)}
|
|
604
|
+
r={hoveredSeriesIndex === i && hoveredPointIndex === pi
|
|
605
|
+
? effectivePointRadius * 1.5
|
|
606
|
+
: effectivePointRadius}
|
|
607
|
+
class={cn(
|
|
608
|
+
'line-chart-point',
|
|
609
|
+
colorClass[seriesColor],
|
|
610
|
+
showGlow && 'has-glow',
|
|
611
|
+
hoveredSeriesIndex === i && hoveredPointIndex === pi && 'is-hovered'
|
|
612
|
+
)}
|
|
613
|
+
onmouseenter={(e) => handlePointHover(point, s.name, e, seriesColor, i, pi)}
|
|
614
|
+
onmouseleave={handlePointLeave}
|
|
615
|
+
onclick={() => handlePointClick(point, s.name, pi)}
|
|
616
|
+
/>
|
|
617
|
+
{/if}
|
|
325
618
|
{/each}
|
|
326
619
|
{/if}
|
|
327
620
|
{/each}
|
|
@@ -333,29 +626,36 @@
|
|
|
333
626
|
{#if tooltipData && isTooltipActive}
|
|
334
627
|
<div
|
|
335
628
|
class="line-chart-tooltip"
|
|
336
|
-
style="
|
|
629
|
+
style="left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;"
|
|
337
630
|
>
|
|
338
|
-
|
|
339
|
-
{
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
631
|
+
{#if tooltipContent}
|
|
632
|
+
{@render tooltipContent({
|
|
633
|
+
point: { x: tooltipData.x, y: tooltipData.y },
|
|
634
|
+
seriesName: tooltipData.seriesName,
|
|
635
|
+
color: tooltipData.color
|
|
636
|
+
})}
|
|
637
|
+
{:else}
|
|
638
|
+
<div class="line-chart-tooltip-content">
|
|
639
|
+
{#if tooltipData.seriesName !== 'Data'}
|
|
640
|
+
<div class="line-chart-tooltip-title">{tooltipData.seriesName}</div>
|
|
641
|
+
{/if}
|
|
642
|
+
<div class="line-chart-tooltip-row">
|
|
643
|
+
<div class="line-chart-tooltip-color is-{tooltipData.color}"></div>
|
|
644
|
+
<span class="line-chart-tooltip-value">
|
|
645
|
+
{formatXValue(tooltipData.x)}, {formatValue(tooltipData.y)}
|
|
646
|
+
</span>
|
|
647
|
+
</div>
|
|
349
648
|
</div>
|
|
350
|
-
|
|
649
|
+
{/if}
|
|
351
650
|
</div>
|
|
352
651
|
{/if}
|
|
353
652
|
|
|
354
|
-
{#if
|
|
355
|
-
<div class=
|
|
356
|
-
{#each normalizedSeries as s}
|
|
357
|
-
|
|
358
|
-
|
|
653
|
+
{#if !hideLegend && legendPosition !== 'none' && normalizedSeries.length > 1}
|
|
654
|
+
<div class={cn('line-chart-legend', legendLayoutClass())}>
|
|
655
|
+
{#each normalizedSeries as s, i}
|
|
656
|
+
{@const seriesColor = getSeriesColor(s, i)}
|
|
657
|
+
<div class={cn('line-chart-legend-item', hoveredSeriesIndex === i && 'is-active')}>
|
|
658
|
+
<div class={cn('line-chart-legend-color', colorClass[seriesColor])}></div>
|
|
359
659
|
<span>{s.name}</span>
|
|
360
660
|
</div>
|
|
361
661
|
{/each}
|