@veristone/nuxt-v-app 0.2.10 → 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.
Files changed (27) hide show
  1. package/app/components/V/A/Card/DonutChart.vue +15 -3
  2. package/app/components/V/A/Card/RevenueBarChart.vue +1 -8
  3. package/app/components/V/A/Chart/AppPerformanceBar.vue +19 -7
  4. package/app/components/V/A/Chart/AppPerformanceBarChart.vue +19 -7
  5. package/app/components/V/A/Chart/AreaMini.vue +1 -2
  6. package/app/components/V/A/Chart/BarMini.vue +0 -1
  7. package/app/components/V/A/Chart/ColorBarChart.vue +8 -5
  8. package/app/components/V/A/Chart/ExpensesBar.vue +1 -1
  9. package/app/components/V/A/Chart/FinanceSummary.vue +14 -7
  10. package/app/components/V/A/Chart/GoogleSearchConsole.vue +0 -1
  11. package/app/components/V/A/Chart/Legend.vue +15 -7
  12. package/app/components/V/A/Chart/Revenue.vue +2 -2
  13. package/app/components/V/A/Chart/RevenueLine.vue +3 -3
  14. package/app/components/V/A/Chart/RevenuevsCost.vue +0 -5
  15. package/app/components/V/A/Chart/SearchIntent.vue +1 -1
  16. package/app/components/V/A/Chart/SpendingTrend.vue +0 -1
  17. package/app/components/V/A/Chart/StockComparisonLine.vue +0 -4
  18. package/app/components/V/A/Chart/StocksPortfolioLine.vue +0 -4
  19. package/app/components/V/A/Chart/StocksSectorLine.vue +0 -4
  20. package/app/components/V/A/Chart/TrafficOverview.vue +2 -3
  21. package/app/components/V/A/Chart/WebPerformanceLineChart.vue +19 -7
  22. package/app/components/V/A/Chart/WinLostDonut.vue +15 -8
  23. package/app/components/V/A/Chart/WinLostLine.vue +0 -5
  24. package/app/composables/useResponsiveHeight.ts +44 -0
  25. package/app/data/AppPerformance.ts +107 -0
  26. package/app/data/WebsiteStatistics.ts +122 -0
  27. 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
- :labels="data"
39
+ :categories="categories"
28
40
  :hide-legend="true"
29
- :radius="0"
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.Bottom"
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: Record<string, BulletLegendItemInterface>
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: Record<string, BulletLegendItemInterface>
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.Bottom"
96
+ :legend-position="LegendPosition.BottomCenter"
98
97
  :hide-y-axis="true"
99
98
  :hide-x-axis="true"
100
99
  :hide-legend="true"
@@ -56,7 +56,6 @@ function formatDate(dateString: Date) {
56
56
  :height="80"
57
57
  :categories="categories"
58
58
  :y-axis="['amount']"
59
- :curve-type="CurveType.Basis"
60
59
  :hide-y-axis="true"
61
60
  :x-formatter="(i) => `${chartData[i]?.month}`"
62
61
  :min-max-ticks-only="false"
@@ -1,8 +1,11 @@
1
1
  <script lang="ts" setup>
2
- const props = defineProps<{
3
- title: string
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.Top"
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
- :labels="
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
- import type { ChartCategories } from '~/types'
2
+ type LegendItem = Pick<BulletLegendItemInterface, 'name' | 'color'>
3
3
 
4
- defineProps<{
5
- categories: ChartCategories
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="(category, categoryKey) in categories"
13
- :key="categoryKey"
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: category.color }"
26
+ :style="{ backgroundColor: getColor(item.color) }"
19
27
  />
20
28
  <span class="text-xs text-(--vis-legend-label-color)">
21
- {{ category.name }}
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.Top"
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="3.5"
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.Top"
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"
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import type { TabsItem } from '#ui/types'
2
+ import type { TabsItem } from '@nuxt/ui'
3
3
 
4
4
  const tabItems: TabsItem[] = [
5
5
  {
@@ -105,7 +105,6 @@ const formatCurrency = (value: number) => {
105
105
  :data="chartData"
106
106
  :height="240"
107
107
  :categories="categories"
108
- :y-axis="['amount']"
109
108
  :y-num-ticks="3"
110
109
  :y-grid-line="true"
111
110
  :curve-type="CurveType.MonotoneX"
@@ -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 + 'K'
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-min="0"
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: Record<string, BulletLegendItemInterface>
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 '0%'
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
- :labels="
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.10",
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,