Add more flexible analytics.
Build and Deploy Web App / build-and-deploy (push) Successful in 20s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 20s
Details
This commit is contained in:
parent
fe2934e2f3
commit
26f0d88f5d
|
|
@ -1,126 +1,89 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AccountApiClient } from '@/api/account'
|
import { type Account, AccountApiClient } from '@/api/account'
|
||||||
import HomeModule from '@/components/HomeModule.vue'
|
import HomeModule from '@/components/HomeModule.vue'
|
||||||
import {
|
import { computed, onMounted, ref, type ComputedRef } from 'vue'
|
||||||
CategoryScale,
|
|
||||||
Chart,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
LinearScale,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
TimeScale,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
type ChartData,
|
|
||||||
type ChartDataset,
|
|
||||||
type ChartOptions,
|
|
||||||
} from 'chart.js'
|
|
||||||
import { getTime } from 'date-fns'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { Line } from 'vue-chartjs'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import 'chartjs-adapter-date-fns'
|
import 'chartjs-adapter-date-fns'
|
||||||
import { integerMoneyToFloat } from '@/api/data'
|
import type { Currency } from '@/api/data'
|
||||||
import { AnalyticsApiClient } from '@/api/analytics'
|
import { AnalyticsApiClient, type BalanceTimeSeriesAnalytics } from '@/api/analytics'
|
||||||
|
import BalanceTimeSeriesChart from './analytics/BalanceTimeSeriesChart.vue'
|
||||||
|
import type { TimeFrame, BalanceTimeSeries } from './analytics/util'
|
||||||
|
import FormGroup from '@/components/common/form/FormGroup.vue'
|
||||||
|
import FormControl from '@/components/common/form/FormControl.vue'
|
||||||
|
import { isAfter, isBefore, startOfMonth, startOfYear, sub } from 'date-fns'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const chartData = ref<ChartData<'line'> | undefined>()
|
const accounts = ref<Account[]>([])
|
||||||
const chartOptions = ref<ChartOptions<'line'> | undefined>()
|
const balanceTimeSeriesData = ref<BalanceTimeSeriesAnalytics>()
|
||||||
|
const currency = ref<Currency>()
|
||||||
|
const timeFrame = ref<TimeFrame>({})
|
||||||
|
|
||||||
Chart.register(
|
const availableCurrencies = computed(() => {
|
||||||
Title,
|
const currencies: Currency[] = []
|
||||||
Tooltip,
|
for (const acc of accounts.value) {
|
||||||
Legend,
|
if (currencies.findIndex(c => c.code === acc.currency.code) === -1) {
|
||||||
LineElement,
|
currencies.push(acc.currency)
|
||||||
CategoryScale,
|
}
|
||||||
LinearScale,
|
}
|
||||||
PointElement,
|
return currencies
|
||||||
TimeScale,
|
})
|
||||||
Filler,
|
const accountBalancesData: ComputedRef<BalanceTimeSeries[]> = computed(() => {
|
||||||
)
|
if (!balanceTimeSeriesData.value) return []
|
||||||
|
const eligibleAccounts = accounts.value.filter(a => a.currency.code === currency.value?.code)
|
||||||
const COLORS = [
|
const series: BalanceTimeSeries[] = []
|
||||||
[255, 69, 69],
|
for (const accountData of balanceTimeSeriesData.value.accounts) {
|
||||||
[255, 160, 69],
|
const account = eligibleAccounts.find(a => a.id === accountData.accountId)
|
||||||
[255, 223, 69],
|
if (account !== undefined) {
|
||||||
[118, 255, 69],
|
const filteredTimeSeries = accountData.balanceTimeSeries.filter(s => {
|
||||||
[69, 255, 188],
|
if (timeFrame.value.start && !isAfter(s.timestamp, timeFrame.value.start)) {
|
||||||
[69, 237, 255],
|
return false
|
||||||
[69, 125, 255],
|
}
|
||||||
[139, 69, 255],
|
if (timeFrame.value.end && !isBefore(s.timestamp, timeFrame.value.end)) {
|
||||||
[251, 69, 255],
|
return false
|
||||||
[255, 69, 167],
|
}
|
||||||
]
|
return true
|
||||||
|
})
|
||||||
|
series.push({
|
||||||
|
label: account.name,
|
||||||
|
snapshots: filteredTimeSeries
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return series
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const api = new AccountApiClient(route)
|
const api = new AccountApiClient(route)
|
||||||
const analyticsApi = new AnalyticsApiClient(route)
|
const analyticsApi = new AnalyticsApiClient(route)
|
||||||
const accounts = await api.getAccounts()
|
accounts.value = await api.getAccounts()
|
||||||
const timeSeriesData = await analyticsApi.getBalanceTimeSeries()
|
balanceTimeSeriesData.value = await analyticsApi.getBalanceTimeSeries()
|
||||||
const datasets: ChartDataset<'line'>[] = []
|
if (accounts.value.length > 0) {
|
||||||
// const timeZoneOffset = -(new Date().getTimezoneOffset())
|
currency.value = accounts.value[0].currency
|
||||||
let colorIdx = 0
|
|
||||||
|
|
||||||
for (const accountData of timeSeriesData.accounts) {
|
|
||||||
if (accountData.currencyCode !== 'USD') continue
|
|
||||||
const account = accounts.find((a) => a.id === accountData.accountId)
|
|
||||||
if (!account) {
|
|
||||||
console.warn("Couldn't find account id " + accountData.accountId)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = COLORS[colorIdx++]
|
|
||||||
const points = accountData.balanceTimeSeries.map((p) => {
|
|
||||||
return {
|
|
||||||
x: getTime(p.timestamp),
|
|
||||||
y: integerMoneyToFloat(p.balance, account.currency),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
datasets.push({
|
|
||||||
label: 'Account #' + account.numberSuffix,
|
|
||||||
data: points,
|
|
||||||
cubicInterpolationMode: 'monotone',
|
|
||||||
borderColor: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
|
|
||||||
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.25)`,
|
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointHoverRadius: 5,
|
|
||||||
pointHitRadius: 5,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
chartData.value = {
|
|
||||||
datasets: datasets,
|
|
||||||
}
|
|
||||||
chartOptions.value = {
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Account Balances',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
unit: 'day',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<HomeModule
|
<HomeModule title="Analytics">
|
||||||
title="Analytics"
|
<BalanceTimeSeriesChart v-if="currency && balanceTimeSeriesData" title="Account Balances" :currency="currency"
|
||||||
style="min-width: 500px"
|
:time-frame="timeFrame" :data="accountBalancesData" />
|
||||||
>
|
|
||||||
<Line
|
<FormGroup>
|
||||||
v-if="chartData && chartOptions"
|
<FormControl label="Currency">
|
||||||
:data="chartData"
|
<select v-model="currency" :disabled="availableCurrencies.length < 2">
|
||||||
:options="chartOptions"
|
<option v-for="currency in availableCurrencies" :key="currency.code" :value="currency">
|
||||||
/>
|
{{ currency.code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Time Frame">
|
||||||
|
<select v-model="timeFrame">
|
||||||
|
<option :value="{}" selected>All Time</option>
|
||||||
|
<option :value="{ start: sub(new Date(), { days: 30 }) }">Last 30 days</option>
|
||||||
|
<option :value="{ start: startOfMonth(new Date()) }">This Month</option>
|
||||||
|
<option :value="{ start: startOfYear(new Date()) }">Year to Date</option>
|
||||||
|
</select>
|
||||||
|
</FormControl>
|
||||||
|
</FormGroup>
|
||||||
</HomeModule>
|
</HomeModule>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CategoryScale, Chart, type ChartData, type ChartDataset, type ChartOptions, Filler, Legend, LinearScale, LineElement, PointElement, TimeScale, Title, Tooltip } from 'chart.js';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import type { BalanceTimeSeries, TimeFrame } from './util';
|
||||||
|
import { integerMoneyToFloat, type Currency } from '@/api/data';
|
||||||
|
import { getTime } from 'date-fns';
|
||||||
|
import { Line } from 'vue-chartjs';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string,
|
||||||
|
timeFrame: TimeFrame,
|
||||||
|
currency: Currency,
|
||||||
|
data: BalanceTimeSeries[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const chartData = computed(() => buildChartData())
|
||||||
|
const chartOptions = ref<ChartOptions<'line'> | undefined>()
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
LineElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
TimeScale,
|
||||||
|
Filler
|
||||||
|
)
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
[255, 69, 69],
|
||||||
|
[255, 160, 69],
|
||||||
|
[255, 223, 69],
|
||||||
|
[118, 255, 69],
|
||||||
|
[69, 255, 188],
|
||||||
|
[69, 237, 255],
|
||||||
|
[69, 125, 255],
|
||||||
|
[139, 69, 255],
|
||||||
|
[251, 69, 255],
|
||||||
|
[255, 69, 167],
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
chartOptions.value = {
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: props.title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'day'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function buildChartData(): ChartData<'line'> {
|
||||||
|
const datasets: ChartDataset<'line'>[] = []
|
||||||
|
let colorIdx = 0
|
||||||
|
|
||||||
|
for (const series of props.data) {
|
||||||
|
const color = COLORS[colorIdx++]
|
||||||
|
if (colorIdx >= COLORS.length) colorIdx = 0
|
||||||
|
|
||||||
|
const points = series.snapshots.map((p) => {
|
||||||
|
return {
|
||||||
|
x: getTime(p.timestamp),
|
||||||
|
y: integerMoneyToFloat(p.balance, props.currency)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
datasets.push({
|
||||||
|
label: series.label,
|
||||||
|
data: points,
|
||||||
|
cubicInterpolationMode: 'monotone',
|
||||||
|
borderColor: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
|
||||||
|
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.25)`,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointHoverRadius: 5,
|
||||||
|
pointHitRadius: 5,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { datasets: datasets }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Line v-if="chartData && chartOptions" :data="chartData" :options="chartOptions" />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { BalanceSnapshot } from '@/api/analytics'
|
||||||
|
|
||||||
|
export interface TimeFrame {
|
||||||
|
start?: Date
|
||||||
|
end?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceTimeSeries {
|
||||||
|
label: string
|
||||||
|
snapshots: BalanceSnapshot[]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue