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">
|
||||
import { AccountApiClient } from '@/api/account'
|
||||
import { type Account, AccountApiClient } from '@/api/account'
|
||||
import HomeModule from '@/components/HomeModule.vue'
|
||||
import {
|
||||
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 { computed, onMounted, ref, type ComputedRef } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import 'chartjs-adapter-date-fns'
|
||||
import { integerMoneyToFloat } from '@/api/data'
|
||||
import { AnalyticsApiClient } from '@/api/analytics'
|
||||
import type { Currency } from '@/api/data'
|
||||
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 chartData = ref<ChartData<'line'> | undefined>()
|
||||
const chartOptions = ref<ChartOptions<'line'> | undefined>()
|
||||
const accounts = ref<Account[]>([])
|
||||
const balanceTimeSeriesData = ref<BalanceTimeSeriesAnalytics>()
|
||||
const currency = ref<Currency>()
|
||||
const timeFrame = ref<TimeFrame>({})
|
||||
|
||||
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],
|
||||
]
|
||||
const availableCurrencies = computed(() => {
|
||||
const currencies: Currency[] = []
|
||||
for (const acc of accounts.value) {
|
||||
if (currencies.findIndex(c => c.code === acc.currency.code) === -1) {
|
||||
currencies.push(acc.currency)
|
||||
}
|
||||
}
|
||||
return currencies
|
||||
})
|
||||
const accountBalancesData: ComputedRef<BalanceTimeSeries[]> = computed(() => {
|
||||
if (!balanceTimeSeriesData.value) return []
|
||||
const eligibleAccounts = accounts.value.filter(a => a.currency.code === currency.value?.code)
|
||||
const series: BalanceTimeSeries[] = []
|
||||
for (const accountData of balanceTimeSeriesData.value.accounts) {
|
||||
const account = eligibleAccounts.find(a => a.id === accountData.accountId)
|
||||
if (account !== undefined) {
|
||||
const filteredTimeSeries = accountData.balanceTimeSeries.filter(s => {
|
||||
if (timeFrame.value.start && !isAfter(s.timestamp, timeFrame.value.start)) {
|
||||
return false
|
||||
}
|
||||
if (timeFrame.value.end && !isBefore(s.timestamp, timeFrame.value.end)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
series.push({
|
||||
label: account.name,
|
||||
snapshots: filteredTimeSeries
|
||||
})
|
||||
}
|
||||
}
|
||||
return series
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const api = new AccountApiClient(route)
|
||||
const analyticsApi = new AnalyticsApiClient(route)
|
||||
const accounts = await api.getAccounts()
|
||||
const timeSeriesData = await analyticsApi.getBalanceTimeSeries()
|
||||
const datasets: ChartDataset<'line'>[] = []
|
||||
// const timeZoneOffset = -(new Date().getTimezoneOffset())
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts.value = await api.getAccounts()
|
||||
balanceTimeSeriesData.value = await analyticsApi.getBalanceTimeSeries()
|
||||
if (accounts.value.length > 0) {
|
||||
currency.value = accounts.value[0].currency
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<HomeModule
|
||||
title="Analytics"
|
||||
style="min-width: 500px"
|
||||
>
|
||||
<Line
|
||||
v-if="chartData && chartOptions"
|
||||
:data="chartData"
|
||||
:options="chartOptions"
|
||||
/>
|
||||
<HomeModule title="Analytics">
|
||||
<BalanceTimeSeriesChart v-if="currency && balanceTimeSeriesData" title="Account Balances" :currency="currency"
|
||||
:time-frame="timeFrame" :data="accountBalancesData" />
|
||||
|
||||
<FormGroup>
|
||||
<FormControl label="Currency">
|
||||
<select v-model="currency" :disabled="availableCurrencies.length < 2">
|
||||
<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>
|
||||
</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