ui-svelte 0.2.11 → 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 +5 -9
- 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 +23 -18
- package/dist/display/Avatar.svelte.d.ts +4 -1
- package/dist/display/AvatarGroup.svelte +20 -18
- package/dist/display/AvatarGroup.svelte.d.ts +6 -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 +30 -11
- 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 -75
- package/dist/display/css/avatar.css +139 -121
- 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/{PasswordStrength.svelte → PasswordField.svelte} +58 -24
- package/dist/form/{PasswordStrength.svelte.d.ts → PasswordField.svelte.d.ts} +6 -5
- package/dist/form/PhoneField.svelte +26 -14
- 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 +1 -1
- 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/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 +3 -3
- 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 +30 -2
- package/dist/icons/index.js +32 -4
- package/dist/index.css +16 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +11 -3
- 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 +3 -3
- 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,4 +1,8 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
1
2
|
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
3
|
+
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
4
|
+
type LegendPosition = 'top' | 'right' | 'bottom' | 'left' | 'none';
|
|
5
|
+
type Palette = 'default' | 'rainbow' | 'ocean' | 'sunset' | 'forest' | 'neon';
|
|
2
6
|
type DataPoint = {
|
|
3
7
|
x: number;
|
|
4
8
|
y: number;
|
|
@@ -20,9 +24,11 @@ type Props = {
|
|
|
20
24
|
margin?: Margin;
|
|
21
25
|
color?: Color;
|
|
22
26
|
colors?: Color[];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
hidePoints?: boolean;
|
|
28
|
+
hideGrid?: boolean;
|
|
29
|
+
hideLegend?: boolean;
|
|
30
|
+
hideXAxis?: boolean;
|
|
31
|
+
hideYAxis?: boolean;
|
|
26
32
|
curve?: 'linear' | 'smooth';
|
|
27
33
|
strokeWidth?: number;
|
|
28
34
|
pointRadius?: number;
|
|
@@ -30,6 +36,23 @@ type Props = {
|
|
|
30
36
|
empty?: boolean;
|
|
31
37
|
emptyText?: string;
|
|
32
38
|
rootClass?: string;
|
|
39
|
+
chartClass?: string;
|
|
40
|
+
size?: Size;
|
|
41
|
+
palette?: Palette;
|
|
42
|
+
legendPosition?: LegendPosition;
|
|
43
|
+
disableAnimation?: boolean;
|
|
44
|
+
animationDuration?: number;
|
|
45
|
+
valueFormatter?: (value: number) => string;
|
|
46
|
+
xFormatter?: (value: number) => string;
|
|
47
|
+
onClick?: (point: DataPoint, seriesName: string, index: number) => void;
|
|
48
|
+
onHover?: (point: DataPoint | null, seriesName: string, index: number) => void;
|
|
49
|
+
tooltipContent?: Snippet<[{
|
|
50
|
+
point: DataPoint;
|
|
51
|
+
seriesName: string;
|
|
52
|
+
color: Color;
|
|
53
|
+
}]>;
|
|
54
|
+
showGradientFill?: boolean;
|
|
55
|
+
showGlow?: boolean;
|
|
33
56
|
};
|
|
34
57
|
declare const LineChart: import("svelte").Component<Props, {}, "">;
|
|
35
58
|
type LineChart = ReturnType<typeof LineChart>;
|
|
@@ -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 Slice = DataPoint & {
|
|
6
11
|
startAngle: number;
|
|
@@ -8,57 +13,97 @@
|
|
|
8
13
|
midAngle: number;
|
|
9
14
|
percentage: number;
|
|
10
15
|
color: Color;
|
|
16
|
+
index: number;
|
|
11
17
|
};
|
|
12
18
|
|
|
13
|
-
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
14
|
-
|
|
15
19
|
type DataPoint = {
|
|
16
20
|
label: string;
|
|
17
21
|
value: number;
|
|
18
22
|
color?: Color;
|
|
23
|
+
disabled?: boolean;
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
type Props = {
|
|
22
27
|
data?: DataPoint[];
|
|
23
|
-
colors?: Color[];
|
|
24
28
|
donut?: boolean;
|
|
25
29
|
donutWidth?: number;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
showPercentages?: boolean;
|
|
30
|
+
size?: Size;
|
|
31
|
+
palette?: Palette;
|
|
32
|
+
legendPosition?: LegendPosition;
|
|
30
33
|
centerLabel?: string;
|
|
31
34
|
centerValue?: string | number;
|
|
32
35
|
startAngle?: number;
|
|
33
36
|
padAngle?: number;
|
|
37
|
+
disableAnimation?: boolean;
|
|
38
|
+
animationDuration?: number;
|
|
39
|
+
showGlow?: boolean;
|
|
40
|
+
showGradient?: boolean;
|
|
41
|
+
hideLabels?: boolean;
|
|
42
|
+
hideValues?: boolean;
|
|
43
|
+
hideLegend?: boolean;
|
|
44
|
+
hidePercentages?: boolean;
|
|
34
45
|
loading?: boolean;
|
|
35
46
|
empty?: boolean;
|
|
36
47
|
emptyText?: string;
|
|
37
48
|
rootClass?: string;
|
|
38
49
|
chartClass?: string;
|
|
50
|
+
valueFormatter?: (value: number) => string;
|
|
51
|
+
onClick?: (slice: DataPoint, index: number) => void;
|
|
52
|
+
onHover?: (slice: DataPoint | null, index: number) => void;
|
|
53
|
+
selected?: number[];
|
|
54
|
+
centerContent?: Snippet;
|
|
55
|
+
tooltipContent?: Snippet<[{ slice: Slice; percentage: number }]>;
|
|
39
56
|
};
|
|
40
57
|
|
|
41
58
|
let {
|
|
42
59
|
data = [],
|
|
43
|
-
colors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'muted'] as Color[],
|
|
44
60
|
donut = false,
|
|
45
61
|
donutWidth = 60,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
showPercentages = true,
|
|
62
|
+
size = 'md',
|
|
63
|
+
palette,
|
|
64
|
+
legendPosition = 'right',
|
|
50
65
|
centerLabel = 'Total',
|
|
51
66
|
centerValue = undefined,
|
|
52
67
|
startAngle = -90,
|
|
53
68
|
padAngle = 0,
|
|
69
|
+
disableAnimation = false,
|
|
70
|
+
animationDuration = 800,
|
|
71
|
+
showGlow = false,
|
|
72
|
+
showGradient = false,
|
|
73
|
+
hideLabels = false,
|
|
74
|
+
hideValues = false,
|
|
75
|
+
hideLegend = false,
|
|
76
|
+
hidePercentages = false,
|
|
54
77
|
loading = false,
|
|
55
78
|
empty = false,
|
|
56
79
|
emptyText = 'No data available',
|
|
57
80
|
rootClass,
|
|
58
|
-
chartClass
|
|
81
|
+
chartClass,
|
|
82
|
+
valueFormatter,
|
|
83
|
+
onClick,
|
|
84
|
+
onHover,
|
|
85
|
+
selected = [],
|
|
86
|
+
centerContent,
|
|
87
|
+
tooltipContent
|
|
59
88
|
}: Props = $props();
|
|
60
89
|
|
|
61
|
-
const
|
|
90
|
+
const sizePresets: Record<Size, { height: number; labelSize: number; valueSize: number }> = {
|
|
91
|
+
sm: { height: 150, labelSize: 10, valueSize: 18 },
|
|
92
|
+
md: { height: 224, labelSize: 12, valueSize: 24 },
|
|
93
|
+
lg: { height: 300, labelSize: 14, valueSize: 32 },
|
|
94
|
+
xl: { height: 400, labelSize: 16, valueSize: 40 }
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const colorPalettes: Record<Palette, Color[]> = {
|
|
98
|
+
default: ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'muted'],
|
|
99
|
+
rainbow: ['danger', 'warning', 'success', 'info', 'primary', 'secondary', 'muted'],
|
|
100
|
+
ocean: ['info', 'primary', 'secondary', 'success', 'muted', 'warning', 'danger'],
|
|
101
|
+
sunset: ['warning', 'danger', 'secondary', 'primary', 'info', 'success', 'muted'],
|
|
102
|
+
forest: ['success', 'primary', 'info', 'secondary', 'muted', 'warning', 'danger'],
|
|
103
|
+
neon: ['secondary', 'primary', 'success', 'warning', 'danger', 'info', 'muted']
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const colorClass: Record<Color, string> = {
|
|
62
107
|
primary: 'is-primary',
|
|
63
108
|
secondary: 'is-secondary',
|
|
64
109
|
success: 'is-success',
|
|
@@ -68,39 +113,71 @@
|
|
|
68
113
|
muted: 'is-muted'
|
|
69
114
|
};
|
|
70
115
|
|
|
116
|
+
function getSliceColor(item: DataPoint, index: number): Color {
|
|
117
|
+
if (item.color) return item.color;
|
|
118
|
+
if (palette) {
|
|
119
|
+
const paletteColors = colorPalettes[palette];
|
|
120
|
+
return paletteColors[index % paletteColors.length];
|
|
121
|
+
}
|
|
122
|
+
return colorPalettes.default[index % colorPalettes.default.length];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatValue(value: number): string {
|
|
126
|
+
if (valueFormatter) return valueFormatter(value);
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
71
130
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
72
131
|
let containerSize = $state({ width: 0, height: 0 });
|
|
132
|
+
let animationFrameId: number | null = null;
|
|
73
133
|
|
|
74
|
-
let
|
|
75
|
-
let
|
|
134
|
+
let tooltipData = $state<Slice | null>(null);
|
|
135
|
+
let tooltipPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
136
|
+
let isTooltipActive = $state(false);
|
|
137
|
+
let hoveredIndex = $state<number | null>(null);
|
|
138
|
+
|
|
139
|
+
let sizeConfig = $derived(sizePresets[size]);
|
|
140
|
+
let effectiveHeight = $derived(sizeConfig.height);
|
|
141
|
+
|
|
142
|
+
let width = $derived(containerSize.width || effectiveHeight);
|
|
143
|
+
let height = $derived(containerSize.height || effectiveHeight);
|
|
144
|
+
let viewBoxSize = $derived(Math.min(width, height) || effectiveHeight);
|
|
76
145
|
|
|
77
146
|
let total = $derived(data.reduce((sum, d) => sum + d.value, 0));
|
|
78
147
|
|
|
148
|
+
// svelte-ignore state_referenced_locally
|
|
149
|
+
let displayPercentages = $state<number[]>(data.map(() => 0));
|
|
150
|
+
|
|
79
151
|
let slices = $derived.by((): Slice[] => {
|
|
80
152
|
const startRad = (startAngle * Math.PI) / 180;
|
|
81
153
|
let currentAngle = startRad;
|
|
82
154
|
const padRad = (padAngle * Math.PI) / 180;
|
|
83
155
|
|
|
84
156
|
return data.map((d, i) => {
|
|
85
|
-
const percentage = (d.value / total) * 100;
|
|
86
|
-
const angle = (d.value / total) * 2 * Math.PI - padRad;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const midAngle =
|
|
157
|
+
const percentage = total > 0 ? (d.value / total) * 100 : 0;
|
|
158
|
+
const angle = total > 0 ? (d.value / total) * 2 * Math.PI - padRad : 0;
|
|
159
|
+
const sliceStartAngle = currentAngle;
|
|
160
|
+
const sliceEndAngle = currentAngle + angle;
|
|
161
|
+
const midAngle = sliceStartAngle + angle / 2;
|
|
90
162
|
|
|
91
|
-
currentAngle =
|
|
163
|
+
currentAngle = sliceEndAngle + padRad;
|
|
92
164
|
|
|
93
165
|
return {
|
|
94
166
|
...d,
|
|
95
|
-
startAngle,
|
|
96
|
-
endAngle,
|
|
167
|
+
startAngle: sliceStartAngle,
|
|
168
|
+
endAngle: sliceEndAngle,
|
|
97
169
|
midAngle,
|
|
98
170
|
percentage,
|
|
99
|
-
color: d
|
|
171
|
+
color: getSliceColor(d, i),
|
|
172
|
+
index: i
|
|
100
173
|
};
|
|
101
174
|
});
|
|
102
175
|
});
|
|
103
176
|
|
|
177
|
+
let gradientIds = $derived(
|
|
178
|
+
data.map((_, i) => `pie-gradient-${i}-${Math.random().toString(36).slice(2, 9)}`)
|
|
179
|
+
);
|
|
180
|
+
|
|
104
181
|
function createArc(
|
|
105
182
|
startAngle: number,
|
|
106
183
|
endAngle: number,
|
|
@@ -130,24 +207,102 @@
|
|
|
130
207
|
return path;
|
|
131
208
|
}
|
|
132
209
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
210
|
+
onMount(() => {
|
|
211
|
+
const updateSize = () => {
|
|
212
|
+
if (containerEl) {
|
|
213
|
+
const rect = containerEl.getBoundingClientRect();
|
|
214
|
+
containerSize = { width: rect.width, height: rect.height };
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleScroll = () => {
|
|
219
|
+
if (isTooltipActive) {
|
|
220
|
+
isTooltipActive = false;
|
|
221
|
+
hoveredIndex = null;
|
|
222
|
+
tooltipData = null;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
updateSize();
|
|
227
|
+
const resizeObserver = new ResizeObserver(updateSize);
|
|
228
|
+
if (containerEl) {
|
|
229
|
+
resizeObserver.observe(containerEl);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
if (animationFrameId !== null) {
|
|
236
|
+
cancelAnimationFrame(animationFrameId);
|
|
237
|
+
}
|
|
238
|
+
resizeObserver.disconnect();
|
|
239
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
$effect(() => {
|
|
244
|
+
const targetPercentages = slices.map((s) => s.percentage / 100);
|
|
245
|
+
|
|
246
|
+
if (animationFrameId !== null) {
|
|
247
|
+
cancelAnimationFrame(animationFrameId);
|
|
248
|
+
animationFrameId = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!disableAnimation) {
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
const startPercentages = untrack(() => [...displayPercentages]);
|
|
254
|
+
|
|
255
|
+
const animate = () => {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const progress = Math.min((now - startTime) / animationDuration, 1);
|
|
258
|
+
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
|
259
|
+
|
|
260
|
+
const newPercentages = startPercentages.map(
|
|
261
|
+
(start, i) => start + ((targetPercentages[i] || 0) - start) * easeProgress
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
displayPercentages = newPercentages;
|
|
265
|
+
|
|
266
|
+
if (progress < 1) {
|
|
267
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
268
|
+
} else {
|
|
269
|
+
animationFrameId = null;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
274
|
+
} else {
|
|
275
|
+
displayPercentages = targetPercentages;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
136
278
|
|
|
137
279
|
function handleSliceHover(slice: Slice, event: MouseEvent): void {
|
|
280
|
+
if (slice.disabled) return;
|
|
281
|
+
|
|
138
282
|
const target = event.target as SVGPathElement;
|
|
139
283
|
const rect = target.getBoundingClientRect();
|
|
140
284
|
|
|
141
285
|
tooltipData = slice;
|
|
142
286
|
tooltipPosition = {
|
|
143
|
-
x: rect.
|
|
144
|
-
y: rect.top
|
|
287
|
+
x: rect.left + rect.width / 2,
|
|
288
|
+
y: rect.top - 10
|
|
145
289
|
};
|
|
146
290
|
isTooltipActive = true;
|
|
291
|
+
hoveredIndex = slice.index;
|
|
292
|
+
|
|
293
|
+
if (onHover) {
|
|
294
|
+
onHover(slice, slice.index);
|
|
295
|
+
}
|
|
147
296
|
}
|
|
148
297
|
|
|
149
298
|
function handleSliceLeave(): void {
|
|
150
299
|
isTooltipActive = false;
|
|
300
|
+
hoveredIndex = null;
|
|
301
|
+
|
|
302
|
+
if (onHover) {
|
|
303
|
+
onHover(null, -1);
|
|
304
|
+
}
|
|
305
|
+
|
|
151
306
|
setTimeout(() => {
|
|
152
307
|
if (!isTooltipActive) {
|
|
153
308
|
tooltipData = null;
|
|
@@ -155,27 +310,57 @@
|
|
|
155
310
|
}, 100);
|
|
156
311
|
}
|
|
157
312
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
};
|
|
313
|
+
function handleSliceClick(slice: Slice): void {
|
|
314
|
+
if (onClick && !slice.disabled) {
|
|
315
|
+
onClick(slice, slice.index);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
165
318
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
319
|
+
let containerLayoutClass = $derived(() => {
|
|
320
|
+
switch (legendPosition) {
|
|
321
|
+
case 'top':
|
|
322
|
+
return 'flex-col-reverse';
|
|
323
|
+
case 'bottom':
|
|
324
|
+
return 'flex-col';
|
|
325
|
+
case 'left':
|
|
326
|
+
return 'flex-row-reverse';
|
|
327
|
+
case 'right':
|
|
328
|
+
default:
|
|
329
|
+
return 'flex-row';
|
|
170
330
|
}
|
|
331
|
+
});
|
|
171
332
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
333
|
+
let legendLayoutClass = $derived(() => {
|
|
334
|
+
switch (legendPosition) {
|
|
335
|
+
case 'top':
|
|
336
|
+
case 'bottom':
|
|
337
|
+
return 'flex-row flex-wrap';
|
|
338
|
+
default:
|
|
339
|
+
return 'flex-col';
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
let responsiveLabelFontSize = $derived(() => {
|
|
344
|
+
const baseSize = 300;
|
|
345
|
+
const scale = viewBoxSize / baseSize;
|
|
346
|
+
return Math.max(8, Math.min(sizeConfig.labelSize * scale, sizeConfig.labelSize));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
let responsiveValueFontSize = $derived(() => {
|
|
350
|
+
const baseSize = 300;
|
|
351
|
+
const scale = viewBoxSize / baseSize;
|
|
352
|
+
return Math.max(14, Math.min(sizeConfig.valueSize * scale, sizeConfig.valueSize));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
let shouldShowCenterText = $derived(viewBoxSize >= 100);
|
|
356
|
+
|
|
357
|
+
let computedCenterValue = $derived(() => {
|
|
358
|
+
const val = centerValue ?? total;
|
|
359
|
+
return valueFormatter && typeof val === 'number' ? valueFormatter(val) : val;
|
|
175
360
|
});
|
|
176
361
|
</script>
|
|
177
362
|
|
|
178
|
-
<div class={cn('pie-chart-container', rootClass)}>
|
|
363
|
+
<div class={cn('pie-chart-container', `is-${size}`, containerLayoutClass(), rootClass)}>
|
|
179
364
|
{#if loading}
|
|
180
365
|
<div class="pie-chart-loading">
|
|
181
366
|
<svg class="pie-chart-loading-spinner" viewBox="0 0 24 24">
|
|
@@ -213,64 +398,107 @@
|
|
|
213
398
|
<span>{emptyText}</span>
|
|
214
399
|
</div>
|
|
215
400
|
{:else}
|
|
216
|
-
<div bind:this={containerEl} class={cn('pie-chart', chartClass)}>
|
|
217
|
-
<svg class="pie-chart-svg">
|
|
401
|
+
<div bind:this={containerEl} class={cn('pie-chart', `is-${size}`, chartClass)}>
|
|
402
|
+
<svg class="pie-chart-svg" viewBox="0 0 {viewBoxSize} {viewBoxSize}">
|
|
403
|
+
{#if showGradient}
|
|
404
|
+
<defs>
|
|
405
|
+
{#each slices as slice, i}
|
|
406
|
+
<linearGradient id={gradientIds[i]} x1="0%" y1="0%" x2="100%" y2="0%">
|
|
407
|
+
<stop offset="0%" style="stop-color:var(--color-{slice.color})" />
|
|
408
|
+
<stop
|
|
409
|
+
offset="100%"
|
|
410
|
+
style="stop-color:var(--color-{slice.color}-dark, var(--color-{slice.color}))"
|
|
411
|
+
/>
|
|
412
|
+
</linearGradient>
|
|
413
|
+
{/each}
|
|
414
|
+
</defs>
|
|
415
|
+
{/if}
|
|
416
|
+
|
|
218
417
|
{#if slices.length > 0}
|
|
219
|
-
{@const centerX =
|
|
220
|
-
{@const centerY =
|
|
221
|
-
{@const radius =
|
|
418
|
+
{@const centerX = viewBoxSize / 2}
|
|
419
|
+
{@const centerY = viewBoxSize / 2}
|
|
420
|
+
{@const radius = viewBoxSize / 2 - 10}
|
|
222
421
|
{@const innerRadius = donut ? radius - donutWidth : 0}
|
|
223
422
|
|
|
224
423
|
<g transform="translate({centerX}, {centerY})">
|
|
225
|
-
{#each slices as slice}
|
|
226
|
-
{@const
|
|
424
|
+
{#each slices as slice, i}
|
|
425
|
+
{@const isSelected = selected.includes(i)}
|
|
426
|
+
{@const isHovered = hoveredIndex === i}
|
|
427
|
+
{@const isDisabled = slice.disabled}
|
|
428
|
+
{@const labelRadius = donut ? radius - donutWidth / 2 : radius * 0.65}
|
|
227
429
|
{@const labelX = Math.cos(slice.midAngle) * labelRadius}
|
|
228
430
|
{@const labelY = Math.sin(slice.midAngle) * labelRadius}
|
|
229
431
|
|
|
230
432
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
433
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
231
434
|
<path
|
|
232
435
|
d={createArc(slice.startAngle, slice.endAngle, innerRadius, radius)}
|
|
233
|
-
class=
|
|
436
|
+
class={cn(
|
|
437
|
+
'pie-chart-slice',
|
|
438
|
+
colorClass[slice.color],
|
|
439
|
+
isSelected && 'is-selected',
|
|
440
|
+
showGlow && 'has-glow',
|
|
441
|
+
isHovered && 'is-hovered',
|
|
442
|
+
isDisabled && 'is-disabled'
|
|
443
|
+
)}
|
|
444
|
+
style={showGradient ? `fill: url(#${gradientIds[i]});` : ''}
|
|
234
445
|
onmouseenter={(e) => handleSliceHover(slice, e)}
|
|
235
446
|
onmouseleave={handleSliceLeave}
|
|
447
|
+
onclick={() => handleSliceClick(slice)}
|
|
236
448
|
/>
|
|
237
449
|
|
|
238
|
-
{#if
|
|
450
|
+
{#if !hideLabels && slice.percentage > 5}
|
|
239
451
|
<text
|
|
240
452
|
x={labelX}
|
|
241
453
|
y={labelY}
|
|
242
454
|
class="pie-chart-slice-label"
|
|
243
455
|
text-anchor="middle"
|
|
244
456
|
dominant-baseline="middle"
|
|
457
|
+
style="font-size: {responsiveLabelFontSize()}px;"
|
|
245
458
|
>
|
|
246
|
-
{#if
|
|
459
|
+
{#if !hidePercentages}
|
|
247
460
|
{slice.percentage.toFixed(1)}%
|
|
248
|
-
{:else if
|
|
249
|
-
{slice.value}
|
|
461
|
+
{:else if !hideValues}
|
|
462
|
+
{formatValue(slice.value)}
|
|
250
463
|
{/if}
|
|
251
464
|
</text>
|
|
252
465
|
{/if}
|
|
253
466
|
{/each}
|
|
254
467
|
|
|
255
|
-
{#if donut}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
468
|
+
{#if donut && shouldShowCenterText}
|
|
469
|
+
{#if centerContent}
|
|
470
|
+
<foreignObject
|
|
471
|
+
x={-innerRadius * 0.7}
|
|
472
|
+
y={-innerRadius * 0.4}
|
|
473
|
+
width={innerRadius * 1.4}
|
|
474
|
+
height={innerRadius * 0.8}
|
|
475
|
+
>
|
|
476
|
+
<div class="pie-chart-center-custom">
|
|
477
|
+
{@render centerContent()}
|
|
478
|
+
</div>
|
|
479
|
+
</foreignObject>
|
|
480
|
+
{:else}
|
|
481
|
+
<text
|
|
482
|
+
x="0"
|
|
483
|
+
y="-8"
|
|
484
|
+
class="pie-chart-center-value"
|
|
485
|
+
text-anchor="middle"
|
|
486
|
+
dominant-baseline="middle"
|
|
487
|
+
style="font-size: {responsiveValueFontSize()}px;"
|
|
488
|
+
>
|
|
489
|
+
{computedCenterValue()}
|
|
490
|
+
</text>
|
|
491
|
+
<text
|
|
492
|
+
x="0"
|
|
493
|
+
y="12"
|
|
494
|
+
class="pie-chart-center-label"
|
|
495
|
+
text-anchor="middle"
|
|
496
|
+
dominant-baseline="middle"
|
|
497
|
+
style="font-size: {responsiveLabelFontSize()}px;"
|
|
498
|
+
>
|
|
499
|
+
{centerLabel}
|
|
500
|
+
</text>
|
|
501
|
+
{/if}
|
|
274
502
|
{/if}
|
|
275
503
|
</g>
|
|
276
504
|
{/if}
|
|
@@ -280,28 +508,41 @@
|
|
|
280
508
|
{#if tooltipData && isTooltipActive}
|
|
281
509
|
<div
|
|
282
510
|
class="pie-chart-tooltip"
|
|
283
|
-
style="
|
|
511
|
+
style="left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;"
|
|
284
512
|
>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
513
|
+
{#if tooltipContent}
|
|
514
|
+
{@render tooltipContent({ slice: tooltipData, percentage: tooltipData.percentage })}
|
|
515
|
+
{:else}
|
|
516
|
+
<div class="pie-chart-tooltip-content">
|
|
517
|
+
<div class="pie-chart-tooltip-title">{tooltipData.label}</div>
|
|
518
|
+
<div class="pie-chart-tooltip-row">
|
|
519
|
+
<div class="pie-chart-tooltip-color is-{tooltipData.color}"></div>
|
|
520
|
+
<span class="pie-chart-tooltip-value">
|
|
521
|
+
{formatValue(tooltipData.value)} ({tooltipData.percentage.toFixed(1)}%)
|
|
522
|
+
</span>
|
|
523
|
+
</div>
|
|
292
524
|
</div>
|
|
293
|
-
|
|
525
|
+
{/if}
|
|
294
526
|
</div>
|
|
295
527
|
{/if}
|
|
296
528
|
|
|
297
|
-
{#if
|
|
298
|
-
<div class=
|
|
299
|
-
{#each slices as slice}
|
|
300
|
-
|
|
529
|
+
{#if !hideLegend && legendPosition !== 'none'}
|
|
530
|
+
<div class={cn('pie-chart-legend', legendLayoutClass())}>
|
|
531
|
+
{#each slices as slice, i}
|
|
532
|
+
{@const isSelected = selected.includes(i)}
|
|
533
|
+
<div
|
|
534
|
+
class={cn('pie-chart-legend-item', isSelected && 'is-selected')}
|
|
535
|
+
onclick={() => handleSliceClick(slice)}
|
|
536
|
+
onkeydown={(e) => e.key === 'Enter' && handleSliceClick(slice)}
|
|
537
|
+
role="button"
|
|
538
|
+
tabindex="0"
|
|
539
|
+
>
|
|
301
540
|
<div class={cn('pie-chart-legend-color', colorClass[slice.color])}></div>
|
|
302
541
|
<span>{slice.label}</span>
|
|
303
|
-
{#if
|
|
304
|
-
<span class="pie-chart-legend-value">
|
|
542
|
+
{#if !hideValues}
|
|
543
|
+
<span class="pie-chart-legend-value">
|
|
544
|
+
({formatValue(slice.value)})
|
|
545
|
+
</span>
|
|
305
546
|
{/if}
|
|
306
547
|
</div>
|
|
307
548
|
{/each}
|
|
@@ -1,27 +1,55 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
1
2
|
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
3
|
+
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
4
|
+
type LegendPosition = 'top' | 'right' | 'bottom' | 'left' | 'none';
|
|
5
|
+
type Palette = 'default' | 'rainbow' | 'ocean' | 'sunset' | 'forest' | 'neon';
|
|
2
6
|
type DataPoint = {
|
|
3
7
|
label: string;
|
|
4
8
|
value: number;
|
|
5
9
|
color?: Color;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
type Slice = DataPoint & {
|
|
13
|
+
startAngle: number;
|
|
14
|
+
endAngle: number;
|
|
15
|
+
midAngle: number;
|
|
16
|
+
percentage: number;
|
|
17
|
+
color: Color;
|
|
18
|
+
index: number;
|
|
6
19
|
};
|
|
7
20
|
type Props = {
|
|
8
21
|
data?: DataPoint[];
|
|
9
|
-
colors?: Color[];
|
|
10
22
|
donut?: boolean;
|
|
11
23
|
donutWidth?: number;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
showPercentages?: boolean;
|
|
24
|
+
size?: Size;
|
|
25
|
+
palette?: Palette;
|
|
26
|
+
legendPosition?: LegendPosition;
|
|
16
27
|
centerLabel?: string;
|
|
17
28
|
centerValue?: string | number;
|
|
18
29
|
startAngle?: number;
|
|
19
30
|
padAngle?: number;
|
|
31
|
+
disableAnimation?: boolean;
|
|
32
|
+
animationDuration?: number;
|
|
33
|
+
showGlow?: boolean;
|
|
34
|
+
showGradient?: boolean;
|
|
35
|
+
hideLabels?: boolean;
|
|
36
|
+
hideValues?: boolean;
|
|
37
|
+
hideLegend?: boolean;
|
|
38
|
+
hidePercentages?: boolean;
|
|
20
39
|
loading?: boolean;
|
|
21
40
|
empty?: boolean;
|
|
22
41
|
emptyText?: string;
|
|
23
42
|
rootClass?: string;
|
|
24
43
|
chartClass?: string;
|
|
44
|
+
valueFormatter?: (value: number) => string;
|
|
45
|
+
onClick?: (slice: DataPoint, index: number) => void;
|
|
46
|
+
onHover?: (slice: DataPoint | null, index: number) => void;
|
|
47
|
+
selected?: number[];
|
|
48
|
+
centerContent?: Snippet;
|
|
49
|
+
tooltipContent?: Snippet<[{
|
|
50
|
+
slice: Slice;
|
|
51
|
+
percentage: number;
|
|
52
|
+
}]>;
|
|
25
53
|
};
|
|
26
54
|
declare const PieChart: import("svelte").Component<Props, {}, "">;
|
|
27
55
|
type PieChart = ReturnType<typeof PieChart>;
|