@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.
- package/dist/components/AccordionItem.vue.d.ts.map +1 -1
- package/dist/components/Avatar.vue.d.ts +6 -1
- package/dist/components/Avatar.vue.d.ts.map +1 -1
- package/dist/components/Badge.vue.d.ts.map +1 -1
- package/dist/components/Card.vue.d.ts +7 -0
- package/dist/components/Card.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/EmptyState.vue.d.ts +43 -0
- package/dist/components/EmptyState.vue.d.ts.map +1 -0
- package/dist/components/Icon/Icon.vue.d.ts +13 -0
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +26 -1
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/ListItem.vue.d.ts +9 -9
- package/dist/components/ListItem.vue.d.ts.map +1 -1
- package/dist/components/Menu.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +3 -3
- package/dist/components/calendar/CalendarPopover.vue.d.ts +10 -0
- package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/charts/BarChart.vue.d.ts +34 -0
- package/dist/components/charts/BarChart.vue.d.ts.map +1 -0
- package/dist/components/charts/ChartTooltip.vue.d.ts +33 -0
- package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -0
- package/dist/components/charts/Donut.vue.d.ts +53 -0
- package/dist/components/charts/Donut.vue.d.ts.map +1 -0
- package/dist/components/charts/Funnel.vue.d.ts +53 -0
- package/dist/components/charts/Funnel.vue.d.ts.map +1 -0
- package/dist/components/charts/Gauge.vue.d.ts +28 -0
- package/dist/components/charts/Gauge.vue.d.ts.map +1 -0
- package/dist/components/charts/LineChart.vue.d.ts +37 -0
- package/dist/components/charts/LineChart.vue.d.ts.map +1 -0
- package/dist/components/charts/RadialBars.vue.d.ts +34 -0
- package/dist/components/charts/RadialBars.vue.d.ts.map +1 -0
- package/dist/components/charts/RankBars.vue.d.ts +27 -0
- package/dist/components/charts/RankBars.vue.d.ts.map +1 -0
- package/dist/components/charts/Sparkline.vue.d.ts +25 -0
- package/dist/components/charts/Sparkline.vue.d.ts.map +1 -0
- package/dist/components/charts/StatCard.vue.d.ts +28 -0
- package/dist/components/charts/StatCard.vue.d.ts.map +1 -0
- package/dist/components/charts/core/data.d.ts +46 -0
- package/dist/components/charts/core/data.d.ts.map +1 -0
- package/dist/components/charts/core/format.d.ts +13 -0
- package/dist/components/charts/core/format.d.ts.map +1 -0
- package/dist/components/charts/core/palette.d.ts +19 -0
- package/dist/components/charts/core/palette.d.ts.map +1 -0
- package/dist/components/charts/core/uid.d.ts +2 -0
- package/dist/components/charts/core/uid.d.ts.map +1 -0
- package/dist/components/charts/core/useChartAnim.d.ts +11 -0
- package/dist/components/charts/core/useChartAnim.d.ts.map +1 -0
- package/dist/components/charts/core/useChartFrame.d.ts +21 -0
- package/dist/components/charts/core/useChartFrame.d.ts.map +1 -0
- package/dist/components/charts/core/useScale.d.ts +16 -0
- package/dist/components/charts/core/useScale.d.ts.map +1 -0
- package/dist/components/charts/index.d.ts +12 -0
- package/dist/components/charts/index.d.ts.map +1 -0
- package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -0
- package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RangeInput.vue.d.ts +13 -4
- package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/Layout.vue.d.ts +1 -1
- package/dist/components/layout/Layout.vue.d.ts.map +1 -1
- package/dist/components/layout/Panel.vue.d.ts +1 -1
- package/dist/components/layout/Panel.vue.d.ts.map +1 -1
- package/dist/components/layout/Timeline.types.d.ts +9 -0
- package/dist/components/layout/Timeline.types.d.ts.map +1 -0
- package/dist/components/layout/Timeline.vue.d.ts +42 -0
- package/dist/components/layout/Timeline.vue.d.ts.map +1 -0
- package/dist/components/layout/TimelineItem.vue.d.ts +37 -0
- package/dist/components/layout/TimelineItem.vue.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/dialog/Dialog.vue.d.ts +4 -0
- package/dist/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/index.cjs +110 -116
- package/dist/index.mjs +38059 -37009
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/AccordionItem.vue +24 -22
- package/src/components/Avatar.vue +49 -11
- package/src/components/Badge.vue +4 -7
- package/src/components/Card.vue +32 -2
- package/src/components/Dropdown.vue +14 -3
- package/src/components/EmptyState.vue +91 -0
- package/src/components/Icon/Icon.vue +118 -25
- package/src/components/Image.vue +70 -3
- package/src/components/ListItem.vue +43 -22
- package/src/components/Menu.vue +10 -2
- package/src/components/charts/BarChart.vue +197 -0
- package/src/components/charts/ChartTooltip.vue +74 -0
- package/src/components/charts/Donut.vue +219 -0
- package/src/components/charts/Funnel.vue +377 -0
- package/src/components/charts/Gauge.vue +90 -0
- package/src/components/charts/LineChart.vue +255 -0
- package/src/components/charts/RadialBars.vue +99 -0
- package/src/components/charts/RankBars.vue +72 -0
- package/src/components/charts/Sparkline.vue +90 -0
- package/src/components/charts/StatCard.vue +84 -0
- package/src/components/charts/core/data.ts +95 -0
- package/src/components/charts/core/format.ts +64 -0
- package/src/components/charts/core/palette.ts +52 -0
- package/src/components/charts/core/uid.ts +6 -0
- package/src/components/charts/core/useChartAnim.ts +60 -0
- package/src/components/charts/core/useChartFrame.ts +49 -0
- package/src/components/charts/core/useScale.ts +39 -0
- package/src/components/charts/index.ts +12 -0
- package/src/components/form/inputs/RadioGroup.vue +2 -1
- package/src/components/form/inputs/RangeInput.vue +43 -15
- package/src/components/form/inputs/SelectInput.vue +1 -19
- package/src/components/index.ts +3 -1
- package/src/components/layout/Timeline.types.ts +9 -0
- package/src/components/layout/Timeline.vue +54 -0
- package/src/components/layout/TimelineItem.vue +93 -0
- package/src/components/layout/index.ts +3 -0
- package/src/dialog/Dialog.vue +29 -1
- package/src/styles/bagel.css +1 -0
- package/src/styles/gradients.css +181 -0
- package/src/styles/layout.css +9 -0
- package/src/styles/theme.css +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +0 -47
- package/dist/components/analytics/BarChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/KpiCard.vue.d.ts +0 -24
- package/dist/components/analytics/KpiCard.vue.d.ts.map +0 -1
- package/dist/components/analytics/LineChart.vue.d.ts +0 -35
- package/dist/components/analytics/LineChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/PieChart.vue.d.ts +0 -53
- package/dist/components/analytics/PieChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/index.d.ts +0 -5
- package/dist/components/analytics/index.d.ts.map +0 -1
- package/src/components/analytics/BarChart.vue +0 -262
- package/src/components/analytics/KpiCard.vue +0 -84
- package/src/components/analytics/LineChart.vue +0 -357
- package/src/components/analytics/PieChart.vue +0 -544
- 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>
|