@bagelink/vue 1.15.63 → 1.15.65

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 (136) hide show
  1. package/dist/components/AccordionItem.vue.d.ts.map +1 -1
  2. package/dist/components/Avatar.vue.d.ts +6 -1
  3. package/dist/components/Avatar.vue.d.ts.map +1 -1
  4. package/dist/components/Badge.vue.d.ts.map +1 -1
  5. package/dist/components/Card.vue.d.ts +7 -0
  6. package/dist/components/Card.vue.d.ts.map +1 -1
  7. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  8. package/dist/components/EmptyState.vue.d.ts +43 -0
  9. package/dist/components/EmptyState.vue.d.ts.map +1 -0
  10. package/dist/components/Icon/Icon.vue.d.ts +13 -0
  11. package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
  12. package/dist/components/Image.vue.d.ts +26 -1
  13. package/dist/components/Image.vue.d.ts.map +1 -1
  14. package/dist/components/ListItem.vue.d.ts +9 -9
  15. package/dist/components/ListItem.vue.d.ts.map +1 -1
  16. package/dist/components/Menu.vue.d.ts.map +1 -1
  17. package/dist/components/Swiper.vue.d.ts +3 -3
  18. package/dist/components/calendar/CalendarPopover.vue.d.ts +10 -0
  19. package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
  20. package/dist/components/charts/BarChart.vue.d.ts +34 -0
  21. package/dist/components/charts/BarChart.vue.d.ts.map +1 -0
  22. package/dist/components/charts/ChartTooltip.vue.d.ts +33 -0
  23. package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -0
  24. package/dist/components/charts/Donut.vue.d.ts +53 -0
  25. package/dist/components/charts/Donut.vue.d.ts.map +1 -0
  26. package/dist/components/charts/Funnel.vue.d.ts +53 -0
  27. package/dist/components/charts/Funnel.vue.d.ts.map +1 -0
  28. package/dist/components/charts/Gauge.vue.d.ts +28 -0
  29. package/dist/components/charts/Gauge.vue.d.ts.map +1 -0
  30. package/dist/components/charts/LineChart.vue.d.ts +37 -0
  31. package/dist/components/charts/LineChart.vue.d.ts.map +1 -0
  32. package/dist/components/charts/RadialBars.vue.d.ts +34 -0
  33. package/dist/components/charts/RadialBars.vue.d.ts.map +1 -0
  34. package/dist/components/charts/RankBars.vue.d.ts +27 -0
  35. package/dist/components/charts/RankBars.vue.d.ts.map +1 -0
  36. package/dist/components/charts/Sparkline.vue.d.ts +25 -0
  37. package/dist/components/charts/Sparkline.vue.d.ts.map +1 -0
  38. package/dist/components/charts/StatCard.vue.d.ts +28 -0
  39. package/dist/components/charts/StatCard.vue.d.ts.map +1 -0
  40. package/dist/components/charts/core/data.d.ts +46 -0
  41. package/dist/components/charts/core/data.d.ts.map +1 -0
  42. package/dist/components/charts/core/format.d.ts +13 -0
  43. package/dist/components/charts/core/format.d.ts.map +1 -0
  44. package/dist/components/charts/core/palette.d.ts +19 -0
  45. package/dist/components/charts/core/palette.d.ts.map +1 -0
  46. package/dist/components/charts/core/uid.d.ts +2 -0
  47. package/dist/components/charts/core/uid.d.ts.map +1 -0
  48. package/dist/components/charts/core/useChartAnim.d.ts +11 -0
  49. package/dist/components/charts/core/useChartAnim.d.ts.map +1 -0
  50. package/dist/components/charts/core/useChartFrame.d.ts +21 -0
  51. package/dist/components/charts/core/useChartFrame.d.ts.map +1 -0
  52. package/dist/components/charts/core/useScale.d.ts +16 -0
  53. package/dist/components/charts/core/useScale.d.ts.map +1 -0
  54. package/dist/components/charts/index.d.ts +12 -0
  55. package/dist/components/charts/index.d.ts.map +1 -0
  56. package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -0
  57. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  58. package/dist/components/form/inputs/RangeInput.vue.d.ts +13 -4
  59. package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
  60. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  61. package/dist/components/index.d.ts +3 -1
  62. package/dist/components/index.d.ts.map +1 -1
  63. package/dist/components/layout/Layout.vue.d.ts +1 -1
  64. package/dist/components/layout/Layout.vue.d.ts.map +1 -1
  65. package/dist/components/layout/Panel.vue.d.ts +1 -1
  66. package/dist/components/layout/Panel.vue.d.ts.map +1 -1
  67. package/dist/components/layout/Timeline.types.d.ts +9 -0
  68. package/dist/components/layout/Timeline.types.d.ts.map +1 -0
  69. package/dist/components/layout/Timeline.vue.d.ts +42 -0
  70. package/dist/components/layout/Timeline.vue.d.ts.map +1 -0
  71. package/dist/components/layout/TimelineItem.vue.d.ts +37 -0
  72. package/dist/components/layout/TimelineItem.vue.d.ts.map +1 -0
  73. package/dist/components/layout/index.d.ts +3 -0
  74. package/dist/components/layout/index.d.ts.map +1 -1
  75. package/dist/dialog/Dialog.vue.d.ts +4 -0
  76. package/dist/dialog/Dialog.vue.d.ts.map +1 -1
  77. package/dist/index.cjs +110 -116
  78. package/dist/index.mjs +38059 -37009
  79. package/dist/style.css +1 -1
  80. package/package.json +2 -1
  81. package/src/components/AccordionItem.vue +24 -22
  82. package/src/components/Avatar.vue +49 -11
  83. package/src/components/Badge.vue +4 -7
  84. package/src/components/Card.vue +32 -2
  85. package/src/components/Dropdown.vue +14 -3
  86. package/src/components/EmptyState.vue +91 -0
  87. package/src/components/Icon/Icon.vue +118 -25
  88. package/src/components/Image.vue +70 -3
  89. package/src/components/ListItem.vue +43 -22
  90. package/src/components/Menu.vue +10 -2
  91. package/src/components/charts/BarChart.vue +197 -0
  92. package/src/components/charts/ChartTooltip.vue +74 -0
  93. package/src/components/charts/Donut.vue +219 -0
  94. package/src/components/charts/Funnel.vue +377 -0
  95. package/src/components/charts/Gauge.vue +90 -0
  96. package/src/components/charts/LineChart.vue +255 -0
  97. package/src/components/charts/RadialBars.vue +99 -0
  98. package/src/components/charts/RankBars.vue +72 -0
  99. package/src/components/charts/Sparkline.vue +90 -0
  100. package/src/components/charts/StatCard.vue +84 -0
  101. package/src/components/charts/core/data.ts +95 -0
  102. package/src/components/charts/core/format.ts +64 -0
  103. package/src/components/charts/core/palette.ts +52 -0
  104. package/src/components/charts/core/uid.ts +6 -0
  105. package/src/components/charts/core/useChartAnim.ts +60 -0
  106. package/src/components/charts/core/useChartFrame.ts +49 -0
  107. package/src/components/charts/core/useScale.ts +39 -0
  108. package/src/components/charts/index.ts +12 -0
  109. package/src/components/form/inputs/RadioGroup.vue +2 -1
  110. package/src/components/form/inputs/RangeInput.vue +43 -15
  111. package/src/components/form/inputs/SelectInput.vue +1 -19
  112. package/src/components/index.ts +3 -1
  113. package/src/components/layout/Timeline.types.ts +9 -0
  114. package/src/components/layout/Timeline.vue +54 -0
  115. package/src/components/layout/TimelineItem.vue +93 -0
  116. package/src/components/layout/index.ts +3 -0
  117. package/src/dialog/Dialog.vue +29 -1
  118. package/src/styles/bagel.css +1 -0
  119. package/src/styles/gradients.css +181 -0
  120. package/src/styles/layout.css +9 -0
  121. package/src/styles/theme.css +1 -1
  122. package/dist/components/analytics/BarChart.vue.d.ts +0 -47
  123. package/dist/components/analytics/BarChart.vue.d.ts.map +0 -1
  124. package/dist/components/analytics/KpiCard.vue.d.ts +0 -24
  125. package/dist/components/analytics/KpiCard.vue.d.ts.map +0 -1
  126. package/dist/components/analytics/LineChart.vue.d.ts +0 -35
  127. package/dist/components/analytics/LineChart.vue.d.ts.map +0 -1
  128. package/dist/components/analytics/PieChart.vue.d.ts +0 -53
  129. package/dist/components/analytics/PieChart.vue.d.ts.map +0 -1
  130. package/dist/components/analytics/index.d.ts +0 -5
  131. package/dist/components/analytics/index.d.ts.map +0 -1
  132. package/src/components/analytics/BarChart.vue +0 -262
  133. package/src/components/analytics/KpiCard.vue +0 -84
  134. package/src/components/analytics/LineChart.vue +0 -357
  135. package/src/components/analytics/PieChart.vue +0 -544
  136. package/src/components/analytics/index.ts +0 -4
@@ -1,262 +0,0 @@
1
- <script setup lang="ts">
2
- import { formatDate, Icon, Loading } from '@bagelink/vue'
3
- import { computed, ref, onMounted, onUnmounted } from 'vue'
4
-
5
- interface SecondaryValue {
6
- label: string
7
- value: number
8
- currency?: boolean
9
- prefix?: string
10
- suffix?: string
11
- }
12
-
13
- interface DataPoint {
14
- date: string
15
- value: number
16
- secondaryValues?: SecondaryValue[]
17
- label?: string
18
- }
19
-
20
- interface Props {
21
- title: string
22
- data: DataPoint[]
23
- icon?: string
24
- color?: string
25
- percentageChange?: number
26
- prefix?: string
27
- suffix?: string
28
- currency?: boolean
29
- maxBars?: number
30
- loading?: boolean
31
- rtl?: boolean
32
- animated?: boolean
33
- animationDuration?: number
34
- animationStartDelay?: number
35
- }
36
-
37
- const props = withDefaults(defineProps<Props>(), {
38
- icon: 'trending_up',
39
- color: 'var(--bgl-primary)',
40
- percentageChange: 0,
41
- prefix: '',
42
- suffix: '',
43
- currency: false,
44
- maxBars: 30,
45
- loading: false,
46
- rtl: false,
47
- animated: true,
48
- animationDuration: 1500,
49
- animationStartDelay: 0,
50
- })
51
-
52
- // Animation state
53
- const animatedProgress = ref(0)
54
- const isAnimating = ref(false)
55
- const isInView = ref(false)
56
- const observer = ref<IntersectionObserver | null>(null)
57
- const chartRef = ref<HTMLElement | null>(null)
58
-
59
- const chartData = computed(() => {
60
- if (!props.data || props.data.length === 0) { return [] }
61
-
62
- // Use all data without limiting to maxBars
63
- const maxValue = Math.max(...props.data.map(d => d.value), 1)
64
- return props.data.map(item => ({
65
- ...item,
66
- height: Math.max((item.value / maxValue) * 100, 2), // Minimum height of 2%
67
- displayLabel: formatDate(item.date, 'MMM')
68
- }))
69
- })
70
-
71
- // Animation computed properties
72
- const getBarOpacity = computed(() => {
73
- return (index: number) => {
74
- if (!props.animated) { return 1 }
75
- if (!isInView.value) { return 0 }
76
-
77
- const totalBars = chartData.value.length
78
-
79
- // Each bar appears with a delay based on its index
80
- const barDelay = index / totalBars
81
- const progress = Math.max(0, Math.min(1, (animatedProgress.value - barDelay) * totalBars))
82
-
83
- return progress
84
- }
85
- })
86
-
87
- // Animation functions
88
- function easeOutCubic(t: number): number {
89
- return 1 - (1 - t) ** 3
90
- }
91
-
92
- function startAnimation() {
93
- if (isAnimating.value || !props.animated) { return }
94
-
95
- console.log(`🎯 TrendChart: Starting animation with ${props.animationDuration}ms duration`)
96
- isAnimating.value = true
97
- animatedProgress.value = 0
98
-
99
- const startTime = performance.now()
100
-
101
- function animate(currentTime: number) {
102
- const elapsed = currentTime - startTime
103
- const progress = Math.min(elapsed / props.animationDuration, 1)
104
- const easedProgress = easeOutCubic(progress)
105
-
106
- animatedProgress.value = easedProgress
107
-
108
- if (progress < 1) {
109
- requestAnimationFrame(animate)
110
- } else {
111
- isAnimating.value = false
112
- console.log(`✅ TrendChart: Animation completed`)
113
- }
114
- }
115
-
116
- requestAnimationFrame(animate)
117
- }
118
-
119
- function setupIntersectionObserver() {
120
- if (!chartRef.value || observer.value) { return }
121
-
122
- observer.value = new IntersectionObserver(
123
- (entries) => {
124
- entries.forEach((entry) => {
125
- if (entry.isIntersecting && !isInView.value) {
126
- console.log(`👀 TrendChart: Entered viewport, starting animation in ${props.animationStartDelay}ms`)
127
- isInView.value = true
128
- setTimeout(() => {
129
- startAnimation()
130
- }, props.animationStartDelay)
131
- }
132
- })
133
- },
134
- {
135
- threshold: 0.3,
136
- rootMargin: '50px'
137
- }
138
- )
139
-
140
- observer.value.observe(chartRef.value)
141
- }
142
-
143
- onMounted(() => {
144
- if (props.animated) {
145
- setupIntersectionObserver()
146
- } else {
147
- // If not animated, show all bars immediately
148
- isInView.value = true
149
- animatedProgress.value = 1
150
- }
151
- })
152
-
153
- onUnmounted(() => {
154
- if (observer.value) {
155
- observer.value.disconnect()
156
- }
157
- })
158
-
159
- function formatValue(value: number, isCurrency: boolean = false): string {
160
- if (isCurrency) {
161
- return new Intl.NumberFormat('he-IL', {
162
- style: 'currency',
163
- currency: 'ILS',
164
- minimumFractionDigits: 0
165
- }).format(value)
166
- }
167
- return value.toLocaleString()
168
- }
169
-
170
- function formatTooltip(item: any): string {
171
- const primaryValue = formatValue(item.value, props.currency)
172
- const primaryText = `${props.prefix}${primaryValue}${props.suffix}`
173
- const tooltipLines = [`${item.displayLabel}`, `<b>${primaryText}</b>`]
174
-
175
- if (item.secondaryValues && Array.isArray(item.secondaryValues)) {
176
- item.secondaryValues.forEach((secondary: SecondaryValue) => {
177
- const formattedSecondaryValue = formatValue(secondary.value, secondary.currency || false)
178
- const secondaryText = `${secondary.prefix || ''}${formattedSecondaryValue}${secondary.suffix || ''}`
179
- tooltipLines.push(`${secondary.label}: <b>${secondaryText}</b>`)
180
- })
181
- }
182
-
183
- return `<div class="trendTooltip">${tooltipLines.join('<p>')}</div>`
184
- }
185
- </script>
186
-
187
- <template>
188
- <div ref="chartRef" class="h-100p flex column flex-stretch">
189
- <div class="flex space-between pb-1">
190
- <div class="flex align-center gap-05">
191
- <Icon :name="icon" size="1.2" :color="color" class="line-height-0" />
192
- <p class="white-space light m_txt14">
193
- {{ title }}
194
- </p>
195
- </div>
196
- <div v-if="percentageChange !== 0" class="flex align-center gap-025">
197
- <Icon
198
- :name="percentageChange > 0 ? 'trending_up' : 'trending_down'" size="1"
199
- :class="percentageChange > 0 ? 'color-success' : 'color-danger'"
200
- />
201
- <span class="txt12 bold" :class="percentageChange > 0 ? 'color-success' : 'color-danger'">
202
- {{ Math.abs(percentageChange) }}%
203
- </span>
204
- </div>
205
- </div>
206
- <div
207
- class="flex w-100p align-items-end mt-auto gap-075 overflow justify-content-start"
208
- :class="[rtl ? 'rtl' : 'ltr']"
209
- >
210
- <div
211
- v-for="(bar, index) in chartData" :key="index" v-tooltip="{ content: formatTooltip(bar), html: true }"
212
- class="flex-grow txt-center hover transition-400 relative barWrap mb-1" :style="{
213
- width: `max(2rem, ${100 / chartData.length}%)`,
214
- opacity: getBarOpacity(index),
215
- transition: animated ? 'opacity 0.3s ease-out' : 'none',
216
- }"
217
- >
218
- <div
219
- class="bar radius-05 transition-400 " :style="{
220
- height: `${bar.height * 1.8}px`,
221
- background: `linear-gradient(180deg, ${color}60, ${color}30)`,
222
- minHeight: '4px',
223
- }"
224
- />
225
- <span
226
- v-if="chartData.length <= 15 || index % Math.ceil(chartData.length / 8) === 0"
227
- class="txt-9 block line-height-1 -bottom-075 white-space color-gray absolute"
228
- :class="rtl ? 'rtl' : 'ltr'"
229
- >
230
- {{ bar.displayLabel }}
231
- </span>
232
- </div>
233
- </div>
234
- <div v-if="loading" class="absolute inset flex justify-content-center rounded">
235
- <Loading v-if="loading" />
236
- </div>
237
- </div>
238
- </template>
239
-
240
- <style>
241
-
242
- .trendTooltip p {
243
- font-weight: 300 !important;
244
- font-size: 12px;
245
- }
246
-
247
- .trendTooltip {
248
- font-weight: 700 !important;
249
- }
250
-
251
- .v-popper--theme-tooltip .v-popper__inner:has(.trendTooltip) {
252
- background-color: var(--bgl-black) !important;
253
- color: var(--bgl-white) !important;
254
- border-radius: 0.5rem !important;
255
- padding: 0.25rem 0.5rem !important;
256
- }
257
-
258
- .bar {
259
- min-width: 2rem;
260
- }
261
-
262
- </style>
@@ -1,84 +0,0 @@
1
- <script setup lang="ts">
2
- import { Card, Icon } from '@bagelink/vue'
3
- import { computed } from 'vue'
4
-
5
- interface Props {
6
- title: string
7
- value: number | string
8
- icon?: string
9
- color?: string
10
- percentageChange?: number
11
- prefix?: string
12
- suffix?: string
13
- currency?: Currency
14
- loading?: boolean
15
- subtitle?: string
16
- }
17
-
18
- type Currency = 'ILS' | 'USD' | 'EUR'
19
-
20
- const props = withDefaults(defineProps<Props>(), {
21
- icon: 'trending_up',
22
- color: 'var(--bgl-primary)',
23
- percentageChange: 0,
24
- prefix: '',
25
- suffix: '',
26
- loading: false,
27
- subtitle: ''
28
- })
29
-
30
- const isIncreasing = computed(() => props.percentageChange >= 0)
31
-
32
- const formattedValue = computed(() => {
33
- if (typeof props.value === 'string') { return props.value }
34
-
35
- if (props.currency) {
36
- return new Intl.NumberFormat('he-IL', {
37
- style: 'currency',
38
- currency: props.currency,
39
- minimumFractionDigits: 0
40
- }).format(props.value)
41
- }
42
-
43
- return props.value.toLocaleString()
44
- })
45
-
46
- const trendColor = computed(() => isIncreasing.value ? 'var(--bgl-green)' : 'var(--bgl-red)'
47
- )
48
- </script>
49
-
50
- <template>
51
- <Card class=" flex column space-between align-items-start py-1 px-1-5 m_p-1 relative ">
52
- <div class="mb-1 flex space-between align-items-start m_mb-05 w-100p">
53
- <div class="flex gap-025">
54
- <Icon :name="icon" size="1" :color="color" class="line-height-0" weight="300" />
55
- <div>
56
- <h3 class="txt14 m-0 line-height-12 light opacity-6">
57
- {{ title }}
58
- </h3>
59
- <p v-if="subtitle" class="txt12 color-gray">
60
- {{ subtitle }}
61
- </p>
62
- </div>
63
- </div>
64
- <div v-if="percentageChange !== 0" class="kpi-trend flex gap-025 txt12 bold ms-auto" :style="{ color: trendColor }">
65
- <Icon :name="isIncreasing ? 'trending_up' : 'trending_down'" />
66
- <span>{{ Math.abs(percentageChange).toFixed(1) }}%</span>
67
- </div>
68
- </div>
69
-
70
- <div class="flex">
71
- <div class="flex align-items-baseline gap-025 w100p" :class="{ loading }">
72
- <span v-if="prefix" class="kpi-prefix txt16 semi color-gray">{{ prefix }}</span>
73
- <span class="kpi-main-value bold txt28 m_txt24 line-height-1">{{ loading ? '...' : formattedValue }}</span>
74
- <span v-if="suffix" class="kpi-suffix txt16 semi color-gray">{{ suffix }}</span>
75
- </div>
76
- </div>
77
- </Card>
78
- </template>
79
-
80
- <style scoped>
81
- .loading .kpi-main-value {
82
- color: var(--bgl-gray);
83
- }
84
- </style>
@@ -1,357 +0,0 @@
1
- <script setup lang="ts">
2
- import { Icon } from '@bagelink/vue'
3
- import { computed, onMounted, onUnmounted, ref } from 'vue'
4
-
5
- interface DataPoint {
6
- date: string
7
- value: number
8
- label?: string
9
- }
10
-
11
- interface Props {
12
- data: DataPoint[]
13
- title?: string
14
- icon?: string
15
- color?: string
16
- height?: number
17
- showPoints?: boolean
18
- currency?: boolean
19
- animated?: boolean
20
- animationDuration?: number
21
- animationStartDelay?: number
22
- percentageChange?: number
23
- }
24
-
25
- const props = withDefaults(defineProps<Props>(), {
26
- title: 'Line Chart',
27
- icon: 'trending_up',
28
- color: 'var(--bgl-primary)',
29
- height: 200,
30
- showPoints: true,
31
- currency: false,
32
- animated: true,
33
- animationDuration: 1500,
34
- animationStartDelay: 0,
35
- percentageChange: 0
36
- })
37
-
38
- const svgRef = ref<SVGElement>()
39
- const chartRef = ref<HTMLElement>()
40
- const width = ref(600)
41
- const height = ref(props.height)
42
-
43
- // Animation state
44
- const animatedProgress = ref(0)
45
- const isAnimating = ref(false)
46
- const isInView = ref(false)
47
-
48
- let observer: IntersectionObserver | null = null
49
-
50
- const padding = { top: 20, inline_end: 30, bottom: 25, inline_start: 60 }
51
- const chartWidth = computed(() => width.value - padding.inline_start - padding.inline_end)
52
- const chartHeight = computed(() => height.value - padding.top - padding.bottom)
53
-
54
- // RTL-aware padding calculations
55
- const paddingLeft = computed(() => {
56
- const isRTL = document.documentElement.dir === 'rtl'
57
- || document.documentElement.getAttribute('lang') === 'he'
58
- return isRTL ? padding.inline_end : padding.inline_start
59
- })
60
-
61
- // RTL-aware label positioning
62
- const labelXPosition = computed(() => {
63
- const isRTL = document.documentElement.dir === 'rtl'
64
- || document.documentElement.getAttribute('lang') === 'he'
65
- return isRTL ? paddingLeft.value + chartWidth.value + 10 : paddingLeft.value - 10
66
- })
67
-
68
- const labelTextAnchor = computed(() => {
69
- const isRTL = document.documentElement.dir === 'rtl'
70
- || document.documentElement.getAttribute('lang') === 'he'
71
- return isRTL ? 'start' : 'end'
72
- })
73
-
74
- const maxValue = computed(() => Math.max(...props.data.map(d => d.value), 0))
75
- const minValue = computed(() => Math.min(...props.data.map(d => d.value), 0))
76
-
77
- const xScale = computed(() => {
78
- const isRTL = document.documentElement.dir === 'rtl'
79
- || document.documentElement.getAttribute('lang') === 'he'
80
- const domain = props.data.length - 1
81
-
82
- return (index: number) => {
83
- const position = (index / domain) * chartWidth.value
84
- // In RTL, reverse the x position so the chart flows from right to left
85
- return isRTL ? chartWidth.value - position : position
86
- }
87
- })
88
-
89
- const yScale = computed(() => {
90
- const range = maxValue.value - minValue.value || 1
91
- return (value: number) => chartHeight.value - ((value - minValue.value) / range) * chartHeight.value
92
- })
93
-
94
- const pathData = computed(() => {
95
- if (props.data.length === 0) { return '' }
96
-
97
- // Calculate how many points to show based on animation progress
98
- const totalPoints = props.data.length
99
- const visiblePoints = props.animated
100
- ? Math.ceil(totalPoints * animatedProgress.value)
101
- : totalPoints
102
-
103
- const dataToShow = props.data.slice(0, Math.max(1, visiblePoints))
104
-
105
- const points = dataToShow.map((d, i) => `${i === 0 ? 'M' : 'L'} ${paddingLeft.value + xScale.value(i)} ${padding.top + yScale.value(d.value)}`
106
- ).join(' ')
107
-
108
- return points
109
- })
110
-
111
- const visiblePoints = computed(() => {
112
- if (!props.animated) { return props.data }
113
-
114
- const totalPoints = props.data.length
115
- const pointsToShow = Math.ceil(totalPoints * animatedProgress.value)
116
- return props.data.slice(0, Math.max(1, pointsToShow))
117
- })
118
-
119
- const gridLines = computed(() => {
120
- const lines = []
121
- const step = chartHeight.value / 4
122
- for (let i = 0; i <= 4; i++) {
123
- const y = i * step
124
- const value = maxValue.value - (i / 4) * (maxValue.value - minValue.value)
125
- lines.push({ y, value })
126
- }
127
- return lines
128
- })
129
-
130
- function formatValue(value: number): string {
131
- if (props.currency) {
132
- return new Intl.NumberFormat('he-IL', {
133
- style: 'currency',
134
- currency: 'ILS',
135
- minimumFractionDigits: 0
136
- }).format(value)
137
- }
138
- return value.toLocaleString()
139
- }
140
-
141
- function formatDate(dateStr: string): string {
142
- const date = new Date(dateStr)
143
- return date.toLocaleDateString('he-IL', { month: 'short', day: 'numeric' })
144
- }
145
-
146
- function formatTooltip(point: any): string {
147
- const tooltipLines = [
148
- `${formatDate(point.date)}`,
149
- `<b>${formatValue(point.value)}</b>`
150
- ]
151
- if (point.label) {
152
- tooltipLines.push(`<b>${point.label}</b>`)
153
- }
154
- return `<div class="lineTooltip">${tooltipLines.join('<p>')}</div>`
155
- }
156
-
157
- function setupIntersectionObserver() {
158
- if (typeof window === 'undefined') { return }
159
-
160
- observer = new IntersectionObserver(
161
- (entries) => {
162
- entries.forEach((entry) => {
163
- if (entry.isIntersecting && !isInView.value) {
164
- isInView.value = true
165
- setTimeout(() => {
166
- startAnimation()
167
- }, 100 + props.animationStartDelay)
168
- }
169
- })
170
- },
171
- {
172
- threshold: 0.3,
173
- rootMargin: '50px'
174
- }
175
- )
176
-
177
- if (chartRef.value) {
178
- observer.observe(chartRef.value)
179
- } else {
180
- setTimeout(() => {
181
- if (chartRef.value && observer) {
182
- observer.observe(chartRef.value)
183
- }
184
- }, 100)
185
- }
186
- }
187
-
188
- function startAnimation() {
189
- console.log(`Starting LineChart animation for "${props.title}" (duration: ${props.animationDuration}ms, start delay: ${props.animationStartDelay}ms)`)
190
- isAnimating.value = true
191
- animatedProgress.value = 0
192
-
193
- const startTime = Date.now()
194
-
195
- const animate = () => {
196
- const elapsed = Date.now() - startTime
197
- const progress = Math.min(elapsed / props.animationDuration, 1)
198
-
199
- animatedProgress.value = easeOutCubic(progress)
200
-
201
- if (progress < 1) {
202
- requestAnimationFrame(animate)
203
- } else {
204
- isAnimating.value = false
205
- console.log(`LineChart animation completed for "${props.title}"`)
206
- }
207
- }
208
-
209
- requestAnimationFrame(animate)
210
- }
211
-
212
- function easeOutCubic(t: number): number {
213
- return 1 - (1 - t) ** 3
214
- }
215
-
216
- onMounted(() => {
217
- if (svgRef.value) {
218
- width.value = svgRef.value.clientWidth || 600
219
- }
220
-
221
- if (props.animated) {
222
- setupIntersectionObserver()
223
- } else {
224
- animatedProgress.value = 1
225
- }
226
- })
227
-
228
- onUnmounted(() => {
229
- if (observer) {
230
- observer.disconnect()
231
- }
232
- })
233
- </script>
234
-
235
- <template>
236
- <div ref="chartRef" class="line-chart h-100p flex column flex-stretch">
237
- <div class="flex space-between pb-1">
238
- <div class="flex align-center gap-05">
239
- <Icon :name="icon" size="1.2" :color="color" class="line-height-0" />
240
- <p class="white-space light m_txt14">
241
- {{ title }}
242
- </p>
243
- </div>
244
- <div v-if="percentageChange !== 0" class="flex align-center gap-025">
245
- <Icon :name="percentageChange > 0 ? 'trending_up' : 'trending_down'" size="1" :class="percentageChange > 0 ? 'color-success' : 'color-danger'" />
246
- <span class="txt12 bold" :class="percentageChange > 0 ? 'color-success' : 'color-danger'">
247
- {{ Math.abs(percentageChange) }}%
248
- </span>
249
- </div>
250
- </div>
251
- <div class="chart-container flex-grow w-100p relative">
252
- <svg ref="svgRef" :width="width" :height="height" class="chart-svg h-100p w-100p">
253
- <!-- Grid lines -->
254
- <g class="grid">
255
- <line v-for="line in gridLines" :key="line.y" :x1="paddingLeft" :y1="padding.top + line.y" :x2="paddingLeft + chartWidth" :y2="padding.top + line.y" stroke="#e0e0e0" stroke-width="1" />
256
- <!-- Y-axis labels -->
257
- <text v-for="line in gridLines" :key="`label-${line.y}`" :x="labelXPosition" :y="padding.top + line.y + 4" class="grid-label" :text-anchor="labelTextAnchor">
258
- {{ formatValue(line.value) }}
259
- </text>
260
- </g>
261
-
262
- <!-- X-axis -->
263
- <line :x1="paddingLeft" :y1="padding.top + chartHeight" :x2="paddingLeft + chartWidth" :y2="padding.top + chartHeight" stroke="#ccc" stroke-width="2" />
264
-
265
- <!-- Y-axis -->
266
- <line :x1="paddingLeft" :y1="padding.top" :x2="paddingLeft" :y2="padding.top + chartHeight" stroke="#ccc" stroke-width="2" />
267
-
268
- <!-- Line path -->
269
- <path :d="pathData" :stroke="color" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round" />
270
-
271
- <!-- Data points -->
272
- <g v-if="showPoints">
273
- <circle
274
- v-for="(point, index) in visiblePoints" :key="index" v-tooltip="{ content: formatTooltip(point), html: true }" :cx="paddingLeft + xScale(index)"
275
- :cy="padding.top + yScale(point.value)" r="4" :fill="color" class="data-point"
276
- />
277
- </g>
278
-
279
- <!-- X-axis labels -->
280
- <g class="x-labels">
281
- <text
282
- v-for="(point, index) in visiblePoints.filter((_, i) => i % Math.ceil(visiblePoints.length / 6) === 0)" :key="index"
283
- :x="paddingLeft + xScale(visiblePoints.findIndex(d => d === point))" :y="padding.top + chartHeight + 15" class="axis-label" text-anchor="middle"
284
- >
285
- {{ formatDate(point.date) }}
286
- </text>
287
- </g>
288
- </svg>
289
- </div>
290
- </div>
291
- </template>
292
-
293
- <style scoped>
294
-
295
- .line-chart,
296
- .chart-container {
297
- direction: inherit;
298
- }
299
-
300
- .chart-svg {
301
- transform-origin: center;
302
- display: block;
303
- /* Remove any inline spacing */
304
- }
305
-
306
- .chart-container {
307
- overflow: hidden;
308
- /* Prevent any overflow spacing */
309
- }
310
-
311
- .grid-label,
312
- .axis-label {
313
- fill: var(--bgl-gray) !important;
314
- ;
315
- font-family: system-ui, -apple-system, sans-serif;
316
- font-size: 12px;
317
- margin: 1rem;
318
- }
319
-
320
- .axis-label {
321
- text-anchor: middle;
322
- }
323
-
324
- .data-point {
325
- cursor: pointer;
326
- transition: r 0.2s ease;
327
- }
328
-
329
- .data-point:hover {
330
- r: 6;
331
- }
332
-
333
- [dir="rtl"] .line-chart {
334
- text-align: right;
335
- }
336
-
337
- </style>
338
-
339
- <style>
340
-
341
- .lineTooltip p {
342
- font-weight: 300 !important;
343
- font-size: 12px;
344
- }
345
-
346
- .lineTooltip {
347
- font-weight: 700 !important;
348
- }
349
-
350
- .v-popper--theme-tooltip .v-popper__inner:has(.lineTooltip) {
351
- background-color: var(--bgl-black) !important;
352
- color: var(--bgl-white) !important;
353
- border-radius: 0.5rem !important;
354
- padding: 0.25rem 0.5rem !important;
355
- }
356
-
357
- </style>