Added working stock price over time comparison chart

This commit is contained in:
Claudio Maggioni 2023-05-21 12:35:37 +02:00
parent 1b9e6c4396
commit 189c8cd2f7
3 changed files with 131 additions and 21 deletions

View File

@ -15,6 +15,7 @@
"core-js": "^3.29.0",
"debounce": "^1.2.1",
"fuse.js": "^6.6.2",
"luxon": "^3.3.0",
"roboto-fontface": "*",
"round-to": "^6.0.0",
"vue": "^3.2.0",
@ -27,6 +28,7 @@
"devDependencies": {
"@babel/types": "^7.21.4",
"@types/debounce": "^1.2.1",
"@types/luxon": "^3.3.0",
"@types/node": "^18.15.0",
"@types/webfontloader": "^1.6.35",
"@vitejs/plugin-vue": "^4.0.0",

View File

@ -8,43 +8,141 @@
</v-app-bar-title>
</v-app-bar>
<v-container class="fill-height">
<h1>Stock price over time</h1>
<ag-charts-vue v-if="options !== null" :options="options" style="width: 100%; height: 30em" />
<v-row>
<v-col cols="12">
<v-card>
<v-card-item>
<v-card-title>Stock price over time</v-card-title>
</v-card-item>
<v-card-text>
<v-btn-toggle class="mb-3" v-model="timeToggle" color="secondary" mandatory>
<v-btn value="1M">1M</v-btn>
<v-btn value="3M">3M</v-btn>
<v-btn value="6M">6M</v-btn>
<v-btn value="1Y">1Y</v-btn>
<v-btn value="5Y">5Y</v-btn>
<v-btn value="MAX">Max</v-btn>
</v-btn-toggle>
<v-skeleton-loader class="chart-loader" v-if="loading" />
<ag-charts-vue class="chart" v-else :options="options" />
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style>
.chart-loader .v-skeleton-loader__bone.v-skeleton-loader__image {
width: 100%;
height: 30em !important;
}
.chart {
width: 100%;
height: 30em !important;
}
</style>
<script setup lang="ts">
import { AgAxisLabelFormatterParams, AgSeriesTooltipRendererParams, time } from 'ag-charts-community';
import { AgChartsVue } from 'ag-charts-vue3';
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { PriceHistory, getPriceHistory } from '@/api';
import { DateTime, DurationLike } from 'luxon';
import { TimeInterval } from 'ag-charts-community/dist/cjs/es5/util/time/interval';
import { roundTo } from 'round-to';
const route = useRoute();
const router = useRouter();
const goHome = () => router.push('/');
const options = ref<object | null>(null);
type TimeOptions = '1M' | '3M' | '6M' | '1Y' | '5Y' | 'MAX';
const timeToggle = ref<TimeOptions>('1M');
const optToDuration: Record<TimeOptions, [DurationLike | undefined, TimeInterval, string]> = {
'1M': [{ months: 1 }, time.day, '%d'],
'3M': [{ months: 3 }, time.friday, '%m-%d'],
'6M': [{ months: 6 }, time.friday.every(2), '%m-%d'],
'1Y': [{ years: 1 }, time.month, '%Y-%m'],
'5Y': [{ years: 5 }, time.month.every(3, {
snapTo: DateTime.utc(2000, 1, 1).toJSDate()
}), '%Y-%m'],
'MAX': [undefined, time.year.every(2), '%Y']
};
const loading = ref<boolean>(true);
const priceHistoryData = ref<PriceHistory[]>([]);
const tickers = ref<string[]>([]);
const colors = ['#E91E63', '#FF9800', '#3F51B5'];
const options = computed(() => {
if (loading.value) return null;
const maxDate = DateTime.now();
const opts = optToDuration[timeToggle.value];
const minDate = opts[0] ? maxDate.minus(opts[0]) : undefined;
const renderer = (params: any) => ({
title: params.title,
content: DateTime.fromMillis(params.xValue).toISODate() + ': ' + roundTo(params.yValue, 2),
});
return {
theme: 'ag-material',
data: priceHistoryData.value.map(e => ({ ...e, date: Date.parse(e.date) })),
series: tickers.value.map((t: string, i: number) => ({
xKey: 'date',
yKey: t,
yName: t,
stroke: colors[i],
tooltip: { renderer: renderer },
marker: {
fill: colors[i],
stroke: colors[i]
}
})),
axes: [
{
type: 'time',
position: 'bottom',
min: minDate?.toJSDate(),
max: maxDate.toJSDate(),
tick: {
interval: opts[1]
},
label: {
autoRotate: true,
autoRotateAngle: 335,
format: opts[2]
}
},
{
type: 'number',
position: 'left',
label: {
format: '$~s',
formatter: (params: AgAxisLabelFormatterParams) =>
params?.formatter?.(params.value).replace('k', 'K').replace('G', 'B') ?? ''
},
},
],
legend: {
position: 'bottom',
},
};
});
// load chart data
onMounted(() => {
const tickers = route.params.tickers.toString().split(',');
console.log(tickers);
getPriceHistory(tickers).then((p: PriceHistory[]) => {
options.value = {
data: p.map(e => ({ ...e, date: Date.parse(e.date) })),
series: tickers.map((t: string) => ({ xKey: 'date', yKey: t })),
axes: [
{
type: 'time',
position: 'bottom',
},
{
type: 'number',
position: 'left',
},
],
};
tickers.value = route.params.tickers.toString().split(',');
getPriceHistory(tickers.value).then((p: PriceHistory[]) => {
loading.value = false;
priceHistoryData.value = p;
});
});

View File

@ -228,6 +228,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/luxon@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.0.tgz#a61043a62c0a72696c73a0a305c544c96501e006"
integrity sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==
"@types/node@^18.15.0":
version "18.16.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01"
@ -1207,6 +1212,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
luxon@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"