@veristone/nuxt-v-app 0.2.9 → 0.2.11
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 +1 -0
- package/app/composables/useResponsiveHeight.ts +44 -0
- package/app/data/AppPerformance.ts +107 -0
- package/app/data/WebsiteStatistics.ts +122 -0
- package/package.json +2 -2
|
@@ -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"
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veristone/nuxt-v-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Veristone Nuxt App Layer - Shared components, composables, and layouts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -68,4 +68,4 @@
|
|
|
68
68
|
"@nuxt/ui": "^4.0.0",
|
|
69
69
|
"nuxt": "^4.0.0"
|
|
70
70
|
}
|
|
71
|
-
}
|
|
71
|
+
}
|