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.
Files changed (206) hide show
  1. package/dist/charts/ArcChart.svelte +9 -13
  2. package/dist/charts/ArcChart.svelte.d.ts +3 -3
  3. package/dist/charts/AreaChart.svelte +347 -118
  4. package/dist/charts/AreaChart.svelte.d.ts +33 -4
  5. package/dist/charts/BarChart.svelte +288 -66
  6. package/dist/charts/BarChart.svelte.d.ts +26 -1
  7. package/dist/charts/Candlestick.svelte +53 -50
  8. package/dist/charts/Candlestick.svelte.d.ts +8 -8
  9. package/dist/charts/LineChart.svelte +391 -91
  10. package/dist/charts/LineChart.svelte.d.ts +26 -3
  11. package/dist/charts/PieChart.svelte +333 -92
  12. package/dist/charts/PieChart.svelte.d.ts +33 -5
  13. package/dist/charts/css/arc-chart.css +3 -3
  14. package/dist/charts/css/area-chart.css +127 -29
  15. package/dist/charts/css/bar-chart.css +114 -8
  16. package/dist/charts/css/candlestick.css +2 -0
  17. package/dist/charts/css/line-chart.css +111 -13
  18. package/dist/charts/css/pie-chart.css +92 -20
  19. package/dist/control/Audio.svelte +86 -44
  20. package/dist/control/Audio.svelte.d.ts +4 -1
  21. package/dist/control/Button.svelte +18 -27
  22. package/dist/control/Button.svelte.d.ts +3 -2
  23. package/dist/control/IconButton.svelte +17 -27
  24. package/dist/control/IconButton.svelte.d.ts +3 -3
  25. package/dist/control/Image.svelte +123 -0
  26. package/dist/control/Image.svelte.d.ts +13 -0
  27. package/dist/control/Record.svelte +144 -98
  28. package/dist/control/Record.svelte.d.ts +2 -1
  29. package/dist/control/ToggleGroup.svelte +22 -8
  30. package/dist/control/ToggleGroup.svelte.d.ts +2 -1
  31. package/dist/control/ToggleTheme.svelte +13 -11
  32. package/dist/control/ToggleTheme.svelte.d.ts +3 -2
  33. package/dist/control/Video.svelte +55 -29
  34. package/dist/control/Video.svelte.d.ts +1 -0
  35. package/dist/control/css/btn.css +200 -152
  36. package/dist/control/css/image.css +56 -0
  37. package/dist/control/css/media.css +95 -30
  38. package/dist/control/css/toggle-group.css +269 -84
  39. package/dist/control/css/video.css +1 -14
  40. package/dist/css/animations.css +427 -2
  41. package/dist/css/base.css +13 -347
  42. package/dist/css/decorations.css +402 -0
  43. package/dist/css/rich-text.css +485 -0
  44. package/dist/css/transitions.css +158 -0
  45. package/dist/css/typography.css +291 -0
  46. package/dist/display/Accordion.svelte +28 -4
  47. package/dist/display/Accordion.svelte.d.ts +2 -1
  48. package/dist/display/Alert.svelte +32 -12
  49. package/dist/display/Alert.svelte.d.ts +2 -3
  50. package/dist/display/Avatar.svelte +54 -26
  51. package/dist/display/Avatar.svelte.d.ts +7 -1
  52. package/dist/display/AvatarGroup.svelte +26 -18
  53. package/dist/display/AvatarGroup.svelte.d.ts +9 -3
  54. package/dist/display/Badge.svelte +11 -4
  55. package/dist/display/Badge.svelte.d.ts +2 -1
  56. package/dist/display/Card.svelte +15 -14
  57. package/dist/display/Card.svelte.d.ts +2 -3
  58. package/dist/display/Carousel.svelte +130 -99
  59. package/dist/display/Carousel.svelte.d.ts +6 -4
  60. package/dist/display/ChatBox.svelte +245 -106
  61. package/dist/display/ChatBox.svelte.d.ts +32 -5
  62. package/dist/display/Chip.svelte +31 -17
  63. package/dist/display/Chip.svelte.d.ts +3 -2
  64. package/dist/display/Code.svelte +6 -3
  65. package/dist/display/Code.svelte.d.ts +1 -0
  66. package/dist/display/Collapsible.svelte +30 -4
  67. package/dist/display/Collapsible.svelte.d.ts +2 -1
  68. package/dist/display/Empty.svelte +37 -3
  69. package/dist/display/Empty.svelte.d.ts +3 -0
  70. package/dist/display/Item.svelte +31 -18
  71. package/dist/display/Item.svelte.d.ts +2 -2
  72. package/dist/display/Map.svelte +488 -0
  73. package/dist/display/Map.svelte.d.ts +44 -0
  74. package/dist/display/Section.svelte +14 -12
  75. package/dist/display/Section.svelte.d.ts +2 -3
  76. package/dist/display/Skeleton.svelte +32 -0
  77. package/dist/display/Skeleton.svelte.d.ts +10 -0
  78. package/dist/display/Table.svelte +94 -132
  79. package/dist/display/Table.svelte.d.ts +10 -1
  80. package/dist/display/css/accordion.css +349 -52
  81. package/dist/display/css/alert.css +18 -25
  82. package/dist/display/css/avatar-group.css +38 -44
  83. package/dist/display/css/avatar.css +152 -123
  84. package/dist/display/css/badge.css +50 -27
  85. package/dist/display/css/card.css +51 -71
  86. package/dist/display/css/carousel.css +25 -5
  87. package/dist/display/css/chat-box.css +158 -26
  88. package/dist/display/css/chip.css +142 -68
  89. package/dist/display/css/code.css +2 -6
  90. package/dist/display/css/collapsible.css +349 -45
  91. package/dist/display/css/divider.css +8 -6
  92. package/dist/display/css/empty.css +7 -0
  93. package/dist/display/css/item.css +311 -89
  94. package/dist/display/css/map.css +164 -0
  95. package/dist/display/css/section.css +78 -33
  96. package/dist/display/css/skeleton.css +58 -0
  97. package/dist/display/css/table.css +320 -189
  98. package/dist/form/Checkbox.svelte +11 -5
  99. package/dist/form/Checkbox.svelte.d.ts +2 -1
  100. package/dist/form/ColorField.svelte +543 -0
  101. package/dist/form/ColorField.svelte.d.ts +29 -0
  102. package/dist/form/ComboBox.svelte +24 -9
  103. package/dist/form/ComboBox.svelte.d.ts +2 -2
  104. package/dist/form/CsvField.svelte +62 -136
  105. package/dist/form/CsvField.svelte.d.ts +2 -2
  106. package/dist/form/DateField.svelte +33 -15
  107. package/dist/form/DateField.svelte.d.ts +2 -1
  108. package/dist/form/DateRange.svelte +436 -0
  109. package/dist/form/DateRange.svelte.d.ts +24 -0
  110. package/dist/form/DragDrop.svelte +348 -0
  111. package/dist/form/DragDrop.svelte.d.ts +32 -0
  112. package/dist/form/Dropzone.svelte +28 -8
  113. package/dist/form/Dropzone.svelte.d.ts +2 -2
  114. package/dist/form/Editor.svelte +626 -0
  115. package/dist/form/Editor.svelte.d.ts +50 -0
  116. package/dist/form/ImageCropper.svelte +291 -61
  117. package/dist/form/ImageCropper.svelte.d.ts +15 -1
  118. package/dist/form/PasswordField.svelte +120 -75
  119. package/dist/form/PasswordField.svelte.d.ts +9 -10
  120. package/dist/form/PhoneField.svelte +34 -16
  121. package/dist/form/PhoneField.svelte.d.ts +4 -3
  122. package/dist/form/PinField.svelte +39 -31
  123. package/dist/form/PinField.svelte.d.ts +3 -3
  124. package/dist/form/RadioGroup.svelte +4 -4
  125. package/dist/form/RadioGroup.svelte.d.ts +1 -1
  126. package/dist/form/Select.svelte +20 -19
  127. package/dist/form/Select.svelte.d.ts +2 -2
  128. package/dist/form/Slider.svelte +4 -2
  129. package/dist/form/Slider.svelte.d.ts +1 -0
  130. package/dist/form/TextField.svelte +16 -7
  131. package/dist/form/TextField.svelte.d.ts +2 -2
  132. package/dist/form/Textarea.svelte +15 -6
  133. package/dist/form/Textarea.svelte.d.ts +2 -2
  134. package/dist/form/Toggle.svelte +11 -1
  135. package/dist/form/Toggle.svelte.d.ts +2 -0
  136. package/dist/form/css/checkbox.css +18 -2
  137. package/dist/form/css/color-field.css +141 -0
  138. package/dist/form/css/control.css +193 -82
  139. package/dist/form/css/csv-field.css +226 -0
  140. package/dist/form/css/date-range.css +122 -0
  141. package/dist/form/css/date.css +24 -2
  142. package/dist/form/css/drag-drop.css +271 -0
  143. package/dist/form/css/dropzone.css +153 -34
  144. package/dist/form/css/editor.css +367 -0
  145. package/dist/form/css/field.css +4 -0
  146. package/dist/form/css/image-cropper.css +223 -22
  147. package/dist/form/css/password.css +1 -1
  148. package/dist/form/css/radio-group.css +1 -1
  149. package/dist/form/css/select.css +2 -2
  150. package/dist/form/css/slider.css +1 -0
  151. package/dist/form/css/textarea.css +178 -75
  152. package/dist/form/css/toggle.css +11 -2
  153. package/dist/hooks/use-table.svelte.d.ts +1 -0
  154. package/dist/hooks/use-table.svelte.js +6 -0
  155. package/dist/icons/index.d.ts +38 -2
  156. package/dist/icons/index.js +40 -4
  157. package/dist/index.css +16 -1
  158. package/dist/index.d.ts +11 -3
  159. package/dist/index.js +10 -2
  160. package/dist/layout/AppBar.svelte +22 -14
  161. package/dist/layout/AppBar.svelte.d.ts +2 -1
  162. package/dist/layout/Footer.svelte +19 -11
  163. package/dist/layout/Footer.svelte.d.ts +2 -1
  164. package/dist/layout/Provider.svelte +27 -4
  165. package/dist/layout/Provider.svelte.d.ts +3 -1
  166. package/dist/layout/css/app-bar.css +63 -66
  167. package/dist/layout/css/footer.css +62 -65
  168. package/dist/navigation/BottomNav.svelte +41 -13
  169. package/dist/navigation/FooterGroup.svelte +1 -1
  170. package/dist/navigation/NavMenu.svelte +47 -23
  171. package/dist/navigation/NavMenu.svelte.d.ts +29 -0
  172. package/dist/navigation/Pagination.svelte +158 -0
  173. package/dist/navigation/Pagination.svelte.d.ts +18 -0
  174. package/dist/navigation/SideNav.svelte +30 -25
  175. package/dist/navigation/SideNav.svelte.d.ts +2 -3
  176. package/dist/navigation/Tabs.svelte +17 -7
  177. package/dist/navigation/Tabs.svelte.d.ts +2 -2
  178. package/dist/navigation/css/bottom-nav.css +279 -257
  179. package/dist/navigation/css/footer-group.css +1 -1
  180. package/dist/navigation/css/footer-nav.css +1 -1
  181. package/dist/navigation/css/nav-menu.css +332 -106
  182. package/dist/navigation/css/pagination.css +74 -0
  183. package/dist/navigation/css/side-nav.css +515 -75
  184. package/dist/navigation/css/tabs.css +246 -52
  185. package/dist/overlay/Command.svelte +340 -0
  186. package/dist/overlay/Command.svelte.d.ts +24 -25
  187. package/dist/overlay/Drawer.svelte +49 -21
  188. package/dist/overlay/Drawer.svelte.d.ts +2 -2
  189. package/dist/overlay/Dropdown.svelte +4 -5
  190. package/dist/overlay/Modal.svelte +51 -16
  191. package/dist/overlay/Modal.svelte.d.ts +3 -3
  192. package/dist/overlay/Toast.svelte +41 -17
  193. package/dist/overlay/Toast.svelte.d.ts +1 -1
  194. package/dist/overlay/Tooltip.svelte +40 -26
  195. package/dist/overlay/Tooltip.svelte.d.ts +2 -2
  196. package/dist/overlay/css/command.css +80 -0
  197. package/dist/overlay/css/drawer.css +63 -24
  198. package/dist/overlay/css/dropdown.css +1 -1
  199. package/dist/overlay/css/hovercard.css +1 -1
  200. package/dist/overlay/css/modal.css +27 -27
  201. package/dist/overlay/css/toast.css +17 -29
  202. package/dist/overlay/css/tooltip.css +83 -66
  203. package/dist/stores/theme.svelte.js +26 -1
  204. package/dist/stores/toast.svelte.d.ts +4 -4
  205. package/dist/stores/toast.svelte.js +2 -2
  206. 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
- showPoints?: boolean;
46
- showGrid?: boolean;
47
- showLegend?: boolean;
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
- showPoints = true,
64
- showGrid = true,
65
- showLegend = false,
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 || 400);
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.color || colors[i % colors.length]
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
- let path = `M ${xScale(points[0].x)} ${yScale(points[0].y)}`;
240
+ const visiblePoints = Math.ceil(points.length * progress);
241
+ const displayPoints = points.slice(0, visiblePoints);
157
242
 
158
- if (smooth && points.length > 1) {
159
- for (let i = 1; i < points.length; i++) {
160
- const x0 = xScale(points[i - 1].x);
161
- const y0 = yScale(points[i - 1].y);
162
- const x1 = xScale(points[i].x);
163
- const y1 = yScale(points[i].y);
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 < points.length; i++) {
169
- path += ` L ${xScale(points[i].x)} ${yScale(points[i].y)}`;
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.right + 8,
202
- y: rect.top + rect.height / 2
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 bind:this={containerEl} class={cn('line-chart-container', rootClass)}>
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="line-chart">
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 showGrid}
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
- <line
286
- x1={0}
287
- y1={innerHeight}
288
- x2={innerWidth}
289
- y2={innerHeight}
290
- class="line-chart-axis-line"
291
- />
292
- <line x1={0} y1={0} x2={0} y2={innerHeight} class="line-chart-axis-line" />
293
-
294
- {#each grid as line}
295
- <text
296
- x={-10}
297
- y={line.y}
298
- class="line-chart-axis-label"
299
- text-anchor="end"
300
- dominant-baseline="middle"
301
- >
302
- {formatNumber(line.value)}
303
- </text>
304
- {/each}
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="line-chart-line is-{s.color}"
311
- style="stroke-width: {strokeWidth};"
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 showPoints}
315
- {#each s.data as point}
316
- <!-- svelte-ignore a11y_no_static_element_interactions -->
317
- <circle
318
- cx={xScale(point.x)}
319
- cy={yScale(point.y)}
320
- r={pointRadius}
321
- class="line-chart-point is-{s.color}"
322
- onmouseenter={(e) => handlePointHover(point, s.name, e, s.color!)}
323
- onmouseleave={handlePointLeave}
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="top: {tooltipPosition.y}px; left: {tooltipPosition.x}px;"
629
+ style="left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;"
337
630
  >
338
- <div class="line-chart-tooltip-content">
339
- {#if tooltipData.seriesName !== 'Data'}
340
- <div class="line-chart-tooltip-title">{tooltipData.seriesName}</div>
341
- {/if}
342
- <div class="line-chart-tooltip-row">
343
- <span class="line-chart-tooltip-label">x:</span>
344
- <span class="line-chart-tooltip-value">{tooltipData.x}</span>
345
- </div>
346
- <div class="line-chart-tooltip-row">
347
- <span class="line-chart-tooltip-label">y:</span>
348
- <span class="line-chart-tooltip-value">{tooltipData.y}</span>
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
- </div>
649
+ {/if}
351
650
  </div>
352
651
  {/if}
353
652
 
354
- {#if showLegend && normalizedSeries.length > 1}
355
- <div class="line-chart-legend">
356
- {#each normalizedSeries as s}
357
- <div class="line-chart-legend-item">
358
- <div class="line-chart-legend-color is-{s.color}"></div>
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}