@veristone/nuxt-v-app 0.2.10 → 0.2.14
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/app/components/V/A/Card/DonutChart.vue +15 -3
- package/app/components/V/A/Card/RevenueBarChart.vue +1 -8
- package/app/components/V/A/Chart/AppPerformanceBar.vue +19 -7
- package/app/components/V/A/Chart/AppPerformanceBarChart.vue +19 -7
- package/app/components/V/A/Chart/AreaMini.vue +1 -2
- package/app/components/V/A/Chart/BarMini.vue +0 -1
- package/app/components/V/A/Chart/ColorBarChart.vue +8 -5
- package/app/components/V/A/Chart/ExpensesBar.vue +1 -1
- package/app/components/V/A/Chart/FinanceSummary.vue +14 -7
- package/app/components/V/A/Chart/GoogleSearchConsole.vue +0 -1
- package/app/components/V/A/Chart/Legend.vue +15 -7
- package/app/components/V/A/Chart/Revenue.vue +2 -2
- package/app/components/V/A/Chart/RevenueLine.vue +3 -3
- package/app/components/V/A/Chart/RevenuevsCost.vue +0 -5
- package/app/components/V/A/Chart/SearchIntent.vue +1 -1
- package/app/components/V/A/Chart/SpendingTrend.vue +0 -1
- package/app/components/V/A/Chart/StockComparisonLine.vue +0 -4
- package/app/components/V/A/Chart/StocksPortfolioLine.vue +0 -4
- package/app/components/V/A/Chart/StocksSectorLine.vue +0 -4
- package/app/components/V/A/Chart/TrafficOverview.vue +2 -3
- package/app/components/V/A/Chart/WebPerformanceLineChart.vue +19 -7
- package/app/components/V/A/Chart/WinLostDonut.vue +15 -8
- package/app/components/V/A/Chart/WinLostLine.vue +0 -5
- package/app/components/V/A/Table/index.vue +63 -70
- package/app/composables/useResponsiveHeight.ts +44 -0
- package/app/data/AppPerformance.ts +107 -0
- package/app/data/WebsiteStatistics.ts +122 -0
- package/package.json +1 -1
|
@@ -5,11 +5,23 @@ interface DonutChartData {
|
|
|
5
5
|
color: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
defineProps<{
|
|
8
|
+
const props = defineProps<{
|
|
9
9
|
data: DonutChartData[];
|
|
10
10
|
title: string;
|
|
11
11
|
totalValue: string;
|
|
12
12
|
}>();
|
|
13
|
+
|
|
14
|
+
const categories = computed<Record<string, BulletLegendItemInterface>>(() =>
|
|
15
|
+
Object.fromEntries(
|
|
16
|
+
props.data.map((item) => [
|
|
17
|
+
item.name,
|
|
18
|
+
{
|
|
19
|
+
name: item.name,
|
|
20
|
+
color: item.color,
|
|
21
|
+
},
|
|
22
|
+
])
|
|
23
|
+
)
|
|
24
|
+
);
|
|
13
25
|
</script>
|
|
14
26
|
<template>
|
|
15
27
|
<UCard>
|
|
@@ -24,9 +36,9 @@ defineProps<{
|
|
|
24
36
|
<DonutChart
|
|
25
37
|
:data="data.map((i) => i.value)"
|
|
26
38
|
:height="200"
|
|
27
|
-
:
|
|
39
|
+
:categories="categories"
|
|
28
40
|
:hide-legend="true"
|
|
29
|
-
:radius="
|
|
41
|
+
:radius="4"
|
|
30
42
|
>
|
|
31
43
|
<div class="absolute text-center">
|
|
32
44
|
<div class="font-semibold">Categories</div>
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const LegendPosition = {
|
|
3
|
-
Top: "top",
|
|
4
|
-
Bottom: "bottom",
|
|
5
|
-
Left: "left",
|
|
6
|
-
Right: "right",
|
|
7
|
-
};
|
|
8
|
-
|
|
9
2
|
interface BarChartCategory {
|
|
10
3
|
name: string;
|
|
11
4
|
color: string;
|
|
@@ -41,7 +34,7 @@ defineProps<{
|
|
|
41
34
|
:radius="4"
|
|
42
35
|
:x-formatter="xFormatter"
|
|
43
36
|
:y-formatter="yFormatter"
|
|
44
|
-
:legend-position="LegendPosition.
|
|
37
|
+
:legend-position="LegendPosition.BottomCenter"
|
|
45
38
|
:hide-legend="false"
|
|
46
39
|
:y-grid-line="false"
|
|
47
40
|
/>
|
|
@@ -5,10 +5,21 @@ import {
|
|
|
5
5
|
getMonthlyPerformanceData
|
|
6
6
|
} from '~/data/AppPerformance'
|
|
7
7
|
|
|
8
|
-
defineProps<{
|
|
9
|
-
categories
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
categories?: Record<string, BulletLegendItemInterface>
|
|
10
10
|
}>()
|
|
11
11
|
|
|
12
|
+
const defaultCategories: Record<string, BulletLegendItemInterface> = {
|
|
13
|
+
subscriptions: {
|
|
14
|
+
name: 'Subscriptions',
|
|
15
|
+
color: 'var(--color-orange-400)'
|
|
16
|
+
},
|
|
17
|
+
downloads: {
|
|
18
|
+
name: 'Downloads',
|
|
19
|
+
color: 'var(--color-lime-400)'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const isSettingsOpen = ref<boolean>(false)
|
|
13
24
|
|
|
14
25
|
function toggleSettings() {
|
|
@@ -32,6 +43,12 @@ const dataViewOptions = [
|
|
|
32
43
|
|
|
33
44
|
const selectedDataView = ref('daily')
|
|
34
45
|
|
|
46
|
+
const categories = computed(() =>
|
|
47
|
+
props.categories && Object.keys(props.categories).length > 0
|
|
48
|
+
? props.categories
|
|
49
|
+
: defaultCategories
|
|
50
|
+
)
|
|
51
|
+
|
|
35
52
|
const performanceData = computed(() => {
|
|
36
53
|
switch (selectedDataView.value) {
|
|
37
54
|
case 'weekly':
|
|
@@ -46,10 +63,6 @@ const performanceData = computed(() => {
|
|
|
46
63
|
|
|
47
64
|
const xFormatter = (i: number): string =>
|
|
48
65
|
String(performanceData.value[i]?.date ?? '')
|
|
49
|
-
|
|
50
|
-
function handleDataViewChange(value: string) {
|
|
51
|
-
console.log('Data view changed to:', value)
|
|
52
|
-
}
|
|
53
66
|
</script>
|
|
54
67
|
|
|
55
68
|
<template>
|
|
@@ -84,7 +97,6 @@ function handleDataViewChange(value: string) {
|
|
|
84
97
|
:items="dataViewOptions"
|
|
85
98
|
placeholder="Select data view"
|
|
86
99
|
class="w-full focus:ring-0"
|
|
87
|
-
@update:model-value="handleDataViewChange"
|
|
88
100
|
/>
|
|
89
101
|
<UButton
|
|
90
102
|
variant="soft"
|
|
@@ -5,10 +5,21 @@ import {
|
|
|
5
5
|
getMonthlyPerformanceData
|
|
6
6
|
} from '~/data/AppPerformance'
|
|
7
7
|
|
|
8
|
-
defineProps<{
|
|
9
|
-
categories
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
categories?: Record<string, BulletLegendItemInterface>
|
|
10
10
|
}>()
|
|
11
11
|
|
|
12
|
+
const defaultCategories: Record<string, BulletLegendItemInterface> = {
|
|
13
|
+
subscriptions: {
|
|
14
|
+
name: 'Subscriptions',
|
|
15
|
+
color: 'var(--color-orange-400)'
|
|
16
|
+
},
|
|
17
|
+
downloads: {
|
|
18
|
+
name: 'Downloads',
|
|
19
|
+
color: 'var(--color-lime-400)'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const isSettingsOpen = ref<boolean>(false)
|
|
13
24
|
|
|
14
25
|
function toggleSettings() {
|
|
@@ -32,6 +43,12 @@ const dataViewOptions = [
|
|
|
32
43
|
|
|
33
44
|
const selectedDataView = ref('daily')
|
|
34
45
|
|
|
46
|
+
const categories = computed(() =>
|
|
47
|
+
props.categories && Object.keys(props.categories).length > 0
|
|
48
|
+
? props.categories
|
|
49
|
+
: defaultCategories
|
|
50
|
+
)
|
|
51
|
+
|
|
35
52
|
const performanceData = computed(() => {
|
|
36
53
|
switch (selectedDataView.value) {
|
|
37
54
|
case 'weekly':
|
|
@@ -46,10 +63,6 @@ const performanceData = computed(() => {
|
|
|
46
63
|
|
|
47
64
|
const xFormatter = (i: number): string =>
|
|
48
65
|
String(performanceData.value[i]?.date ?? '')
|
|
49
|
-
|
|
50
|
-
function handleDataViewChange(value: string) {
|
|
51
|
-
console.log('Data view changed to:', value)
|
|
52
|
-
}
|
|
53
66
|
</script>
|
|
54
67
|
|
|
55
68
|
<template>
|
|
@@ -84,7 +97,6 @@ function handleDataViewChange(value: string) {
|
|
|
84
97
|
:items="dataViewOptions"
|
|
85
98
|
placeholder="Select data view"
|
|
86
99
|
class="w-full focus:ring-0"
|
|
87
|
-
@update:model-value="handleDataViewChange"
|
|
88
100
|
/>
|
|
89
101
|
<UButton
|
|
90
102
|
variant="soft"
|
|
@@ -91,10 +91,9 @@ const formatCurrency = (value: number) => {
|
|
|
91
91
|
:data="chartData"
|
|
92
92
|
:height="90"
|
|
93
93
|
:categories="categories"
|
|
94
|
-
:y-axis="['amount']"
|
|
95
94
|
:y-num-ticks="2"
|
|
96
95
|
:y-grid-line="true"
|
|
97
|
-
:legend-position="LegendPosition.
|
|
96
|
+
:legend-position="LegendPosition.BottomCenter"
|
|
98
97
|
:hide-y-axis="true"
|
|
99
98
|
:hide-x-axis="true"
|
|
100
99
|
:hide-legend="true"
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
const props = defineProps<{
|
|
3
|
-
title
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
title?: string
|
|
4
4
|
color?: string
|
|
5
|
-
}>()
|
|
5
|
+
}>(), {
|
|
6
|
+
title: 'Performance Metrics',
|
|
7
|
+
color: 'var(--ui-primary)'
|
|
8
|
+
})
|
|
6
9
|
|
|
7
10
|
const RevenueData = [
|
|
8
11
|
{
|
|
@@ -37,12 +40,12 @@ const RevenueData = [
|
|
|
37
40
|
}
|
|
38
41
|
]
|
|
39
42
|
|
|
40
|
-
const RevenueCategories = {
|
|
43
|
+
const RevenueCategories = computed(() => ({
|
|
41
44
|
desktop: {
|
|
42
45
|
name: 'Desktop',
|
|
43
46
|
color: props.color
|
|
44
47
|
}
|
|
45
|
-
}
|
|
48
|
+
}))
|
|
46
49
|
|
|
47
50
|
const xFormatter = (i: number): string => `${RevenueData[i]?.month}`
|
|
48
51
|
const yFormatter = (i: number) => i.toString()
|
|
@@ -44,7 +44,7 @@ const yFormatter = (value: number): string => `$${(value / 1000).toFixed(1)}k`;
|
|
|
44
44
|
:y-axis="['sales' ,'counter']"
|
|
45
45
|
:x-formatter="xFormatter"
|
|
46
46
|
:y-formatter="yFormatter"
|
|
47
|
-
:legend-position="LegendPosition.
|
|
47
|
+
:legend-position="LegendPosition.TopCenter"
|
|
48
48
|
:hide-legend="false"
|
|
49
49
|
:y-grid-line="true"
|
|
50
50
|
:x-grid-line="false"
|
|
@@ -28,6 +28,19 @@ const spendingData = ref<SpendingCategory[]>([
|
|
|
28
28
|
},
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
|
+
const spendingCategories = computed<Record<string, BulletLegendItemInterface>>(
|
|
32
|
+
() =>
|
|
33
|
+
Object.fromEntries(
|
|
34
|
+
spendingData.value.map((item) => [
|
|
35
|
+
item.name,
|
|
36
|
+
{
|
|
37
|
+
name: item.name,
|
|
38
|
+
color: item.color,
|
|
39
|
+
},
|
|
40
|
+
])
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
31
44
|
const totalSpent = computed(() =>
|
|
32
45
|
spendingData.value.reduce((sum, item) => sum + item.value, 0)
|
|
33
46
|
);
|
|
@@ -49,13 +62,7 @@ const formatCurrency = (amount: number) => {
|
|
|
49
62
|
:data="spendingData.map((i) => i.value)"
|
|
50
63
|
:height="200"
|
|
51
64
|
:arc-width="10"
|
|
52
|
-
:
|
|
53
|
-
spendingData.map((item) => ({
|
|
54
|
-
name: item.name,
|
|
55
|
-
color: item.color,
|
|
56
|
-
percentage: item.value,
|
|
57
|
-
}))
|
|
58
|
-
"
|
|
65
|
+
:categories="spendingCategories"
|
|
59
66
|
:pad-angle="0.1"
|
|
60
67
|
:hide-legend="true"
|
|
61
68
|
:radius="4"
|
|
@@ -83,7 +83,6 @@ const xFormatter = (tick: number, _i?: number, _ticks?: number[]): string => {
|
|
|
83
83
|
:group-padding="0.2"
|
|
84
84
|
:bar-padding="0.5"
|
|
85
85
|
:y-axis="['clicks', 'impressions', 'averagePosition', 'ctr']"
|
|
86
|
-
:curve-type="CurveType.Linear"
|
|
87
86
|
:legend-position="LegendPosition.BottomCenter"
|
|
88
87
|
/>
|
|
89
88
|
</UCard>
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
|
|
2
|
+
type LegendItem = Pick<BulletLegendItemInterface, 'name' | 'color'>
|
|
3
3
|
|
|
4
|
-
defineProps<{
|
|
5
|
-
categories
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
categories?: Record<string, BulletLegendItemInterface> | null
|
|
6
|
+
items?: LegendItem[] | null
|
|
6
7
|
}>()
|
|
8
|
+
|
|
9
|
+
const legendEntries = computed(() =>
|
|
10
|
+
props.items?.length ? props.items : Object.values(props.categories ?? {})
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const getColor = (color?: string | string[]) =>
|
|
14
|
+
Array.isArray(color) ? color[0] ?? 'currentColor' : color ?? 'currentColor'
|
|
7
15
|
</script>
|
|
8
16
|
|
|
9
17
|
<template>
|
|
10
18
|
<div class="flex flex-wrap items-center justify-end gap-x-6 gap-y-2">
|
|
11
19
|
<div
|
|
12
|
-
v-for="(
|
|
13
|
-
:key="
|
|
20
|
+
v-for="(item, index) in legendEntries"
|
|
21
|
+
:key="`${item.name}-${index}`"
|
|
14
22
|
class="flex items-center gap-2"
|
|
15
23
|
>
|
|
16
24
|
<span
|
|
17
25
|
class="h-2 w-2 rounded-full"
|
|
18
|
-
:style="{ backgroundColor:
|
|
26
|
+
:style="{ backgroundColor: getColor(item.color) }"
|
|
19
27
|
/>
|
|
20
28
|
<span class="text-xs text-(--vis-legend-label-color)">
|
|
21
|
-
{{
|
|
29
|
+
{{ item.name }}
|
|
22
30
|
</span>
|
|
23
31
|
</div>
|
|
24
32
|
</div>
|
|
@@ -36,8 +36,8 @@ const yFormatter = (tick: number, _i?: number, _ticks?: number[]) =>
|
|
|
36
36
|
:y-num-ticks="4"
|
|
37
37
|
:x-formatter="xFormatter"
|
|
38
38
|
:y-formatter="yFormatter"
|
|
39
|
-
:legend-position="LegendPosition.
|
|
39
|
+
:legend-position="LegendPosition.TopCenter"
|
|
40
40
|
:hide-legend="true"
|
|
41
41
|
:y-grid-line="true"
|
|
42
42
|
/>
|
|
43
|
-
</template>
|
|
43
|
+
</template>
|
|
@@ -29,14 +29,14 @@ const yFormatter = (value: number): string => `$${(value / 1000).toFixed(1)}k`;
|
|
|
29
29
|
:data="chartData"
|
|
30
30
|
:height="180"
|
|
31
31
|
:x-num-ticks="4"
|
|
32
|
-
:y-num-ticks="
|
|
32
|
+
:y-num-ticks="4"
|
|
33
33
|
:categories="categories"
|
|
34
34
|
:x-formatter="xFormatter"
|
|
35
35
|
:y-formatter="yFormatter"
|
|
36
36
|
:curve-type="CurveType.Linear"
|
|
37
|
-
:legend-position="LegendPosition.
|
|
37
|
+
:legend-position="LegendPosition.TopCenter"
|
|
38
38
|
:y-grid-line="true"
|
|
39
39
|
:hide-x-axis="true"
|
|
40
40
|
:hide-legend="true"
|
|
41
41
|
/>
|
|
42
|
-
</template>
|
|
42
|
+
</template>
|
|
@@ -70,13 +70,8 @@ const { height } = useResponsiveHeight({
|
|
|
70
70
|
<LineChart
|
|
71
71
|
:data="chartData"
|
|
72
72
|
:categories="chartCategories"
|
|
73
|
-
:y-axis="['Income', 'Expense']"
|
|
74
73
|
:x-formatter="xAxisFormatter"
|
|
75
74
|
:height="height"
|
|
76
|
-
:stacked="false"
|
|
77
|
-
:radius="3"
|
|
78
|
-
:group-padding="0.2"
|
|
79
|
-
:bar-padding="0.2"
|
|
80
75
|
:x-grid-line="true"
|
|
81
76
|
:y-grid-line="true"
|
|
82
77
|
:y-num-ticks="4"
|
|
@@ -70,12 +70,8 @@ const xAxisFormatter = (tick: number): string => {
|
|
|
70
70
|
<LineChart
|
|
71
71
|
:data="chartData"
|
|
72
72
|
:categories="chartCategories"
|
|
73
|
-
:y-axis="['stockA', 'stockB', 'stockC']"
|
|
74
73
|
:x-formatter="xAxisFormatter"
|
|
75
74
|
:height="220"
|
|
76
|
-
:radius="3"
|
|
77
|
-
:group-padding="0.2"
|
|
78
|
-
:bar-padding="0.2"
|
|
79
75
|
:x-grid-line="true"
|
|
80
76
|
:y-grid-line="true"
|
|
81
77
|
:x-domain-line="true"
|
|
@@ -145,13 +145,9 @@ const xAxisFormatter = (tick: number): string => {
|
|
|
145
145
|
<LineChart
|
|
146
146
|
:data="chartData"
|
|
147
147
|
:categories="chartCategories"
|
|
148
|
-
:y-axis="['portfolioValue']"
|
|
149
148
|
:x-formatter="xAxisFormatter"
|
|
150
149
|
:y-formatter="(i: number) => i / 1000 + 'K'"
|
|
151
150
|
:height="220"
|
|
152
|
-
:radius="3"
|
|
153
|
-
:group-padding="0.2"
|
|
154
|
-
:bar-padding="0.2"
|
|
155
151
|
:x-grid-line="true"
|
|
156
152
|
:y-grid-line="true"
|
|
157
153
|
:x-domain-line="true"
|
|
@@ -207,13 +207,9 @@ const xAxisFormatter = (tick: number): string => {
|
|
|
207
207
|
<LineChart
|
|
208
208
|
:data="chartData"
|
|
209
209
|
:categories="chartCategories"
|
|
210
|
-
:y-axis="['domestic', 'international']"
|
|
211
210
|
:x-formatter="xAxisFormatter"
|
|
212
211
|
:y-formatter="(i: number) => i / 1000 + 'K'"
|
|
213
212
|
:height="220"
|
|
214
|
-
:radius="3"
|
|
215
|
-
:group-padding="0.2"
|
|
216
|
-
:bar-padding="0.2"
|
|
217
213
|
:x-grid-line="true"
|
|
218
214
|
:y-grid-line="true"
|
|
219
215
|
:x-domain-line="true"
|
|
@@ -67,7 +67,7 @@ const categories: Record<string, Category> = {
|
|
|
67
67
|
|
|
68
68
|
const xFormatter = (i: number) => `${engagementData[i]?.date}`
|
|
69
69
|
|
|
70
|
-
const yFormatter = (value: number) => value
|
|
70
|
+
const yFormatter = (value: number) => `${value.toFixed(1)}m`
|
|
71
71
|
</script>
|
|
72
72
|
|
|
73
73
|
<template>
|
|
@@ -95,8 +95,7 @@ const yFormatter = (value: number) => value + 'K'
|
|
|
95
95
|
:y-num-ticks="3"
|
|
96
96
|
:x-formatter="xFormatter"
|
|
97
97
|
:y-formatter="yFormatter"
|
|
98
|
-
:y-
|
|
99
|
-
:y-max="600000"
|
|
98
|
+
:y-domain="[0, 8]"
|
|
100
99
|
:x-grid-line="true"
|
|
101
100
|
:legend-position="LegendPosition.BottomCenter"
|
|
102
101
|
:curve-type="CurveType.Linear"
|
|
@@ -5,10 +5,21 @@ import {
|
|
|
5
5
|
getMonthlyWebsiteStatistics
|
|
6
6
|
} from '~/data/WebsiteStatistics'
|
|
7
7
|
|
|
8
|
-
defineProps<{
|
|
9
|
-
categories
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
categories?: Record<string, BulletLegendItemInterface>
|
|
10
10
|
}>()
|
|
11
11
|
|
|
12
|
+
const defaultCategories: Record<string, BulletLegendItemInterface> = {
|
|
13
|
+
conversions: {
|
|
14
|
+
name: 'Conversions',
|
|
15
|
+
color: 'var(--color-orange-400)'
|
|
16
|
+
},
|
|
17
|
+
bounceRate: {
|
|
18
|
+
name: 'Bounce Rate',
|
|
19
|
+
color: 'var(--color-lime-400)'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const isSettingsOpen = ref<boolean>(false)
|
|
13
24
|
|
|
14
25
|
function toggleSettings() {
|
|
@@ -32,6 +43,12 @@ const dataViewOptions = [
|
|
|
32
43
|
|
|
33
44
|
const selectedDataView = ref('daily')
|
|
34
45
|
|
|
46
|
+
const categories = computed(() =>
|
|
47
|
+
props.categories && Object.keys(props.categories).length > 0
|
|
48
|
+
? props.categories
|
|
49
|
+
: defaultCategories
|
|
50
|
+
)
|
|
51
|
+
|
|
35
52
|
const WebsiteStatistics = computed(() => {
|
|
36
53
|
switch (selectedDataView.value) {
|
|
37
54
|
case 'weekly':
|
|
@@ -46,10 +63,6 @@ const WebsiteStatistics = computed(() => {
|
|
|
46
63
|
|
|
47
64
|
const xFormatter = (i: number): string =>
|
|
48
65
|
String(WebsiteStatistics.value[i]?.date ?? '')
|
|
49
|
-
|
|
50
|
-
function handleDataViewChange(value: string) {
|
|
51
|
-
console.log('Data view changed to:', value)
|
|
52
|
-
}
|
|
53
66
|
</script>
|
|
54
67
|
|
|
55
68
|
<template>
|
|
@@ -84,7 +97,6 @@ function handleDataViewChange(value: string) {
|
|
|
84
97
|
:items="dataViewOptions"
|
|
85
98
|
placeholder="Select data view"
|
|
86
99
|
class="w-full focus:ring-0"
|
|
87
|
-
@update:model-value="handleDataViewChange"
|
|
88
100
|
/>
|
|
89
101
|
<UButton
|
|
90
102
|
variant="soft"
|
|
@@ -36,9 +36,22 @@ const winLossData = computed(() => [
|
|
|
36
36
|
}
|
|
37
37
|
])
|
|
38
38
|
|
|
39
|
+
const winLossCategories = computed<Record<string, BulletLegendItemInterface>>(
|
|
40
|
+
() =>
|
|
41
|
+
Object.fromEntries(
|
|
42
|
+
winLossData.value.map(item => [
|
|
43
|
+
item.name,
|
|
44
|
+
{
|
|
45
|
+
name: item.name,
|
|
46
|
+
color: item.color
|
|
47
|
+
}
|
|
48
|
+
])
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
39
52
|
const percentage = computed(() => {
|
|
40
53
|
const total = win.value + loss.value
|
|
41
|
-
if (total === 0) return
|
|
54
|
+
if (total === 0) return 0
|
|
42
55
|
return Math.round((win.value / total) * 100)
|
|
43
56
|
})
|
|
44
57
|
</script>
|
|
@@ -56,13 +69,7 @@ const percentage = computed(() => {
|
|
|
56
69
|
:data="winLossData.map((i) => i.value)"
|
|
57
70
|
:height="160"
|
|
58
71
|
:arc-width="10"
|
|
59
|
-
:
|
|
60
|
-
winLossData.map((item) => ({
|
|
61
|
-
name: item.name,
|
|
62
|
-
color: item.color,
|
|
63
|
-
percentage: item.value
|
|
64
|
-
}))
|
|
65
|
-
"
|
|
72
|
+
:categories="winLossCategories"
|
|
66
73
|
:pad-angle="0.1"
|
|
67
74
|
:hide-legend="true"
|
|
68
75
|
:radius="4"
|
|
@@ -83,13 +83,8 @@ const xAxisFormatter = (tick: number): string => {
|
|
|
83
83
|
<LineChart
|
|
84
84
|
:data="chartData"
|
|
85
85
|
:categories="chartCategories"
|
|
86
|
-
:y-axis="['Income', 'Expense']"
|
|
87
86
|
:x-formatter="xAxisFormatter"
|
|
88
87
|
:height="160"
|
|
89
|
-
:stacked="false"
|
|
90
|
-
:radius="3"
|
|
91
|
-
:group-padding="0.2"
|
|
92
|
-
:bar-padding="0.2"
|
|
93
88
|
:hide-legend="true"
|
|
94
89
|
:x-grid-line="true"
|
|
95
90
|
:y-grid-line="true"
|
|
@@ -52,11 +52,12 @@
|
|
|
52
52
|
<USelect
|
|
53
53
|
v-for="filter in props.filters"
|
|
54
54
|
:key="filter.key"
|
|
55
|
-
|
|
55
|
+
:model-value="(filterValues[filter.key] as string)"
|
|
56
56
|
:items="filter.options"
|
|
57
57
|
:placeholder="filter.label"
|
|
58
58
|
size="sm"
|
|
59
59
|
class="w-40"
|
|
60
|
+
@update:model-value="(v: string) => filterValues[filter.key] = v"
|
|
60
61
|
/>
|
|
61
62
|
</template>
|
|
62
63
|
|
|
@@ -166,50 +167,44 @@
|
|
|
166
167
|
</template>
|
|
167
168
|
|
|
168
169
|
<script setup lang="ts">
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
*
|
|
208
|
-
* @exposes
|
|
209
|
-
* selectedRows - Array of selected row originals
|
|
210
|
-
* rowSelection - Raw TanStack row selection state
|
|
211
|
-
* clearSelection - Clears the current row selection
|
|
212
|
-
*/
|
|
170
|
+
import type { SortingState } from '@tanstack/vue-table'
|
|
171
|
+
import { getPaginationRowModel } from '@tanstack/vue-table'
|
|
172
|
+
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
|
|
173
|
+
import CellRenderer from './CellRenderer.vue'
|
|
174
|
+
|
|
175
|
+
interface FilterOption {
|
|
176
|
+
key: string
|
|
177
|
+
label: string
|
|
178
|
+
options: { label: string; value: string }[]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const props = withDefaults(
|
|
182
|
+
defineProps<{
|
|
183
|
+
name?: string
|
|
184
|
+
data?: Record<string, unknown>[]
|
|
185
|
+
columns: TableColumn<any>[]
|
|
186
|
+
loading?: boolean
|
|
187
|
+
initialPageLimit?: number
|
|
188
|
+
selectable?: boolean
|
|
189
|
+
showSearch?: boolean
|
|
190
|
+
filters?: FilterOption[]
|
|
191
|
+
onRefresh?: () => Promise<void>
|
|
192
|
+
onRowClick?: (row: any) => void
|
|
193
|
+
defaultSort?: string
|
|
194
|
+
defaultSortDesc?: boolean
|
|
195
|
+
manualPagination?: boolean
|
|
196
|
+
total?: number
|
|
197
|
+
}>(),
|
|
198
|
+
{
|
|
199
|
+
data: () => [],
|
|
200
|
+
loading: false,
|
|
201
|
+
initialPageLimit: 10,
|
|
202
|
+
selectable: false,
|
|
203
|
+
showSearch: true,
|
|
204
|
+
defaultSortDesc: false,
|
|
205
|
+
manualPagination: false,
|
|
206
|
+
}
|
|
207
|
+
)
|
|
213
208
|
|
|
214
209
|
// Sorting state - v-model support
|
|
215
210
|
const sorting = defineModel<SortingState>("sorting", {
|
|
@@ -397,41 +392,40 @@ const tableColumns = computed(() => {
|
|
|
397
392
|
'expiresAt', 'completedAt', 'created_at', 'updated_at'
|
|
398
393
|
]
|
|
399
394
|
|
|
400
|
-
const columns = props.columns.map(col => {
|
|
401
|
-
const
|
|
402
|
-
const
|
|
395
|
+
const columns: TableColumn<any>[] = props.columns.map((col) => {
|
|
396
|
+
const accessorKey = (col as any).accessorKey as string | undefined
|
|
397
|
+
const hasDataAccessor = Boolean(accessorKey || (col as any).accessorFn || (col as any).key)
|
|
398
|
+
const colDef: TableColumn<any> = {
|
|
403
399
|
...col,
|
|
404
400
|
enableSorting: col.enableSorting ?? hasDataAccessor,
|
|
405
|
-
}
|
|
401
|
+
}
|
|
406
402
|
|
|
407
|
-
|
|
408
|
-
const isDateColumn = dateColumnPatterns.includes(col.accessorKey)
|
|
403
|
+
const isDateColumn = accessorKey ? dateColumnPatterns.includes(accessorKey) : false
|
|
409
404
|
const hasPreset = col.meta?.preset && col.meta.preset !== 'actions'
|
|
410
405
|
|
|
411
406
|
if (hasPreset || isDateColumn) {
|
|
412
|
-
// Use existing preset or auto-detected date preset
|
|
413
407
|
const preset = col.meta?.preset || 'date'
|
|
414
408
|
const format = col.meta?.format || 'relative'
|
|
415
409
|
|
|
416
|
-
colDef.cell = ({ row }) => {
|
|
417
|
-
const value = row?.original?.[
|
|
410
|
+
colDef.cell = ({ row }: { row: any }) => {
|
|
411
|
+
const value = accessorKey ? row?.original?.[accessorKey] : undefined
|
|
418
412
|
return h(CellRenderer, {
|
|
419
413
|
value,
|
|
420
414
|
column: { ...col, meta: { ...col.meta, preset, format } },
|
|
421
415
|
row: row?.original
|
|
422
|
-
})
|
|
423
|
-
}
|
|
416
|
+
})
|
|
417
|
+
}
|
|
424
418
|
}
|
|
425
419
|
|
|
426
|
-
return colDef
|
|
427
|
-
})
|
|
420
|
+
return colDef
|
|
421
|
+
})
|
|
428
422
|
|
|
429
423
|
if (props.selectable) {
|
|
430
|
-
const selectColumn: TableColumn<
|
|
424
|
+
const selectColumn: TableColumn<any> = {
|
|
431
425
|
id: 'select',
|
|
432
426
|
enableSorting: false,
|
|
433
427
|
enableHiding: false,
|
|
434
|
-
header: ({ table }) =>
|
|
428
|
+
header: ({ table }: { table: any }) =>
|
|
435
429
|
h(UCheckbox, {
|
|
436
430
|
modelValue: table.getIsSomePageRowsSelected()
|
|
437
431
|
? 'indeterminate'
|
|
@@ -440,7 +434,7 @@ const tableColumns = computed(() => {
|
|
|
440
434
|
table.toggleAllPageRowsSelected(!!value),
|
|
441
435
|
'aria-label': 'Select all rows',
|
|
442
436
|
}),
|
|
443
|
-
cell: ({ row }) =>
|
|
437
|
+
cell: ({ row }: { row: any }) =>
|
|
444
438
|
h(UCheckbox, {
|
|
445
439
|
modelValue: row.getIsSelected(),
|
|
446
440
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
|
|
@@ -453,21 +447,20 @@ const tableColumns = computed(() => {
|
|
|
453
447
|
td: 'w-10',
|
|
454
448
|
},
|
|
455
449
|
},
|
|
456
|
-
}
|
|
450
|
+
}
|
|
457
451
|
|
|
458
|
-
columns.unshift(selectColumn)
|
|
452
|
+
columns.unshift(selectColumn)
|
|
459
453
|
}
|
|
460
454
|
|
|
461
455
|
// Add actions column if slot provided AND no actions column already exists
|
|
462
|
-
if (slots['actions-cell'] && !columns.some(c => c.id === 'actions' || c.accessorKey === 'actions')) {
|
|
463
|
-
const actionsColumn: TableColumn<
|
|
456
|
+
if (slots['actions-cell'] && !columns.some((c: any) => c.id === 'actions' || c.accessorKey === 'actions')) {
|
|
457
|
+
const actionsColumn: TableColumn<any> = {
|
|
464
458
|
id: 'actions',
|
|
465
459
|
header: 'Actions',
|
|
466
460
|
enableSorting: false,
|
|
467
461
|
enableHiding: false,
|
|
468
|
-
cell: ({ row }) => {
|
|
469
|
-
|
|
470
|
-
return h('div', {}, slots['actions-cell']?.({ row }));
|
|
462
|
+
cell: ({ row }: { row: any }) => {
|
|
463
|
+
return h('div', {}, slots['actions-cell']?.({ row }))
|
|
471
464
|
},
|
|
472
465
|
meta: {
|
|
473
466
|
class: {
|
|
@@ -475,9 +468,9 @@ const tableColumns = computed(() => {
|
|
|
475
468
|
td: 'text-right',
|
|
476
469
|
},
|
|
477
470
|
},
|
|
478
|
-
}
|
|
471
|
+
}
|
|
479
472
|
|
|
480
|
-
columns.push(actionsColumn)
|
|
473
|
+
columns.push(actionsColumn)
|
|
481
474
|
}
|
|
482
475
|
|
|
483
476
|
return columns;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface ResponsiveHeightOptions {
|
|
5
|
+
default: number
|
|
6
|
+
sm?: number
|
|
7
|
+
md?: number
|
|
8
|
+
lg?: number
|
|
9
|
+
xl?: number
|
|
10
|
+
'2xl'?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useResponsiveHeight(options: ResponsiveHeightOptions) {
|
|
14
|
+
const breakpoints = useBreakpoints(breakpointsTailwind)
|
|
15
|
+
|
|
16
|
+
const height = computed(() => {
|
|
17
|
+
if (
|
|
18
|
+
options['2xl'] !== undefined
|
|
19
|
+
&& breakpoints.greaterOrEqual('2xl').value
|
|
20
|
+
) {
|
|
21
|
+
return options['2xl']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.xl !== undefined && breakpoints.greaterOrEqual('xl').value) {
|
|
25
|
+
return options.xl
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.lg !== undefined && breakpoints.greaterOrEqual('lg').value) {
|
|
29
|
+
return options.lg
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (options.md !== undefined && breakpoints.greaterOrEqual('md').value) {
|
|
33
|
+
return options.md
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.sm !== undefined && breakpoints.greaterOrEqual('sm').value) {
|
|
37
|
+
return options.sm
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return options.default
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return { height }
|
|
44
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface AppPerformanceItem {
|
|
4
|
+
date: string
|
|
5
|
+
subscriptions: number
|
|
6
|
+
downloads: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const startDate = new Date('2025-01-01T00:00:00Z')
|
|
10
|
+
|
|
11
|
+
const sourceDailyData: AppPerformanceItem[] = Array.from(
|
|
12
|
+
{ length: 90 },
|
|
13
|
+
(_, index) => {
|
|
14
|
+
const date = new Date(startDate)
|
|
15
|
+
date.setUTCDate(startDate.getUTCDate() + index)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
date: date.toISOString().slice(0, 10),
|
|
19
|
+
subscriptions: Math.max(
|
|
20
|
+
48,
|
|
21
|
+
86
|
|
22
|
+
+ Math.round(Math.sin(index / 2.8) * 22)
|
|
23
|
+
+ index
|
|
24
|
+
+ ((index % 6) - 2) * 4
|
|
25
|
+
),
|
|
26
|
+
downloads: Math.max(
|
|
27
|
+
96,
|
|
28
|
+
148
|
|
29
|
+
+ Math.round(Math.cos(index / 3.4) * 28)
|
|
30
|
+
+ index * 2
|
|
31
|
+
+ ((index % 5) - 2) * 6
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export const getDailyPerformanceData = computed(() => {
|
|
38
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
39
|
+
month: 'short',
|
|
40
|
+
day: '2-digit'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return sourceDailyData.slice(0, 30).map(item => ({
|
|
44
|
+
...item,
|
|
45
|
+
date: formatter.format(new Date(item.date))
|
|
46
|
+
}))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const getWeeklyPerformanceData = computed((): AppPerformanceItem[] => {
|
|
50
|
+
const aggregated = new Map<
|
|
51
|
+
number,
|
|
52
|
+
{ subscriptions: number, downloads: number }
|
|
53
|
+
>()
|
|
54
|
+
|
|
55
|
+
for (let index = 0; index < sourceDailyData.length; index++) {
|
|
56
|
+
const item = sourceDailyData[index]
|
|
57
|
+
if (!item) continue
|
|
58
|
+
|
|
59
|
+
const week = Math.floor(index / 7) + 1
|
|
60
|
+
|
|
61
|
+
if (!aggregated.has(week)) {
|
|
62
|
+
aggregated.set(week, {
|
|
63
|
+
subscriptions: 0,
|
|
64
|
+
downloads: 0
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const current = aggregated.get(week)!
|
|
69
|
+
current.subscriptions += item.subscriptions
|
|
70
|
+
current.downloads += item.downloads
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Array.from(aggregated.entries()).map(([week, value]) => ({
|
|
74
|
+
date: `Week ${String(week).padStart(2, '0')}`,
|
|
75
|
+
subscriptions: value.subscriptions,
|
|
76
|
+
downloads: value.downloads
|
|
77
|
+
}))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const getMonthlyPerformanceData = computed((): AppPerformanceItem[] => {
|
|
81
|
+
const aggregated = new Map<string, { subscriptions: number, downloads: number }>()
|
|
82
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
83
|
+
month: 'short',
|
|
84
|
+
year: '2-digit'
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
for (const item of sourceDailyData) {
|
|
88
|
+
const monthKey = item.date.slice(0, 7)
|
|
89
|
+
|
|
90
|
+
if (!aggregated.has(monthKey)) {
|
|
91
|
+
aggregated.set(monthKey, {
|
|
92
|
+
subscriptions: 0,
|
|
93
|
+
downloads: 0
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const current = aggregated.get(monthKey)!
|
|
98
|
+
current.subscriptions += item.subscriptions
|
|
99
|
+
current.downloads += item.downloads
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Array.from(aggregated.entries()).map(([monthKey, value]) => ({
|
|
103
|
+
date: formatter.format(new Date(`${monthKey}-01`)),
|
|
104
|
+
subscriptions: value.subscriptions,
|
|
105
|
+
downloads: value.downloads
|
|
106
|
+
}))
|
|
107
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface WebsitePerformanceMetric {
|
|
4
|
+
date: string
|
|
5
|
+
bounceRate: number
|
|
6
|
+
conversions: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const startDate = new Date('2025-01-01T00:00:00Z')
|
|
10
|
+
|
|
11
|
+
const sourceDailyData: WebsitePerformanceMetric[] = Array.from(
|
|
12
|
+
{ length: 90 },
|
|
13
|
+
(_, index) => {
|
|
14
|
+
const date = new Date(startDate)
|
|
15
|
+
date.setUTCDate(startDate.getUTCDate() + index)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
date: date.toISOString().slice(0, 10),
|
|
19
|
+
bounceRate: Math.max(
|
|
20
|
+
28,
|
|
21
|
+
Math.min(
|
|
22
|
+
62,
|
|
23
|
+
46
|
|
24
|
+
+ Math.round(Math.sin(index / 4) * 5)
|
|
25
|
+
- (index % 6)
|
|
26
|
+
)
|
|
27
|
+
),
|
|
28
|
+
conversions: Math.max(
|
|
29
|
+
72,
|
|
30
|
+
138
|
|
31
|
+
+ Math.round(Math.cos(index / 3.1) * 18)
|
|
32
|
+
+ Math.round(index * 0.7)
|
|
33
|
+
+ ((index % 4) - 1) * 6
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export const getDailyWebsiteStatistics = computed(() => {
|
|
40
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
41
|
+
month: 'short',
|
|
42
|
+
day: '2-digit'
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return sourceDailyData.slice(0, 30).map(item => ({
|
|
46
|
+
...item,
|
|
47
|
+
date: formatter.format(new Date(item.date))
|
|
48
|
+
}))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const getWeeklyWebsiteStatistics = computed(
|
|
52
|
+
(): WebsitePerformanceMetric[] => {
|
|
53
|
+
const aggregated = new Map<
|
|
54
|
+
number,
|
|
55
|
+
{ bounceRate: number, conversions: number, count: number }
|
|
56
|
+
>()
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < sourceDailyData.length; index++) {
|
|
59
|
+
const item = sourceDailyData[index]
|
|
60
|
+
if (!item) continue
|
|
61
|
+
|
|
62
|
+
const week = Math.floor(index / 7) + 1
|
|
63
|
+
|
|
64
|
+
if (!aggregated.has(week)) {
|
|
65
|
+
aggregated.set(week, {
|
|
66
|
+
bounceRate: 0,
|
|
67
|
+
conversions: 0,
|
|
68
|
+
count: 0
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const current = aggregated.get(week)!
|
|
73
|
+
current.bounceRate += item.bounceRate
|
|
74
|
+
current.conversions += item.conversions
|
|
75
|
+
current.count++
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Array.from(aggregated.entries()).map(([week, value]) => ({
|
|
79
|
+
date: `Week ${String(week).padStart(2, '0')}`,
|
|
80
|
+
bounceRate: Math.round(value.bounceRate / value.count),
|
|
81
|
+
conversions: value.conversions
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
export const getMonthlyWebsiteStatistics = computed(
|
|
87
|
+
(): WebsitePerformanceMetric[] => {
|
|
88
|
+
const aggregated = new Map<
|
|
89
|
+
string,
|
|
90
|
+
{ bounceRate: number, conversions: number, count: number }
|
|
91
|
+
>()
|
|
92
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
93
|
+
month: 'short',
|
|
94
|
+
year: '2-digit'
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
for (const item of sourceDailyData) {
|
|
98
|
+
const monthKey = item.date.slice(0, 7)
|
|
99
|
+
|
|
100
|
+
if (!aggregated.has(monthKey)) {
|
|
101
|
+
aggregated.set(monthKey, {
|
|
102
|
+
bounceRate: 0,
|
|
103
|
+
conversions: 0,
|
|
104
|
+
count: 0
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const current = aggregated.get(monthKey)!
|
|
109
|
+
current.bounceRate += item.bounceRate
|
|
110
|
+
current.conversions += item.conversions
|
|
111
|
+
current.count++
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Array.from(aggregated.entries()).map(([monthKey, value]) => ({
|
|
115
|
+
date: formatter.format(new Date(`${monthKey}-01`)),
|
|
116
|
+
bounceRate: Math.round(value.bounceRate / value.count),
|
|
117
|
+
conversions: value.conversions
|
|
118
|
+
}))
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
export const WebsiteStatistics = getMonthlyWebsiteStatistics
|