Added working stock price over time comparison chart
This commit is contained in:
parent
1b9e6c4396
commit
189c8cd2f7
3 changed files with 131 additions and 21 deletions
|
@ -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",
|
||||
|
|
|
@ -8,44 +8,142 @@
|
|||
</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';
|
||||
|
||||
// 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 })),
|
||||
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(() => {
|
||||
tickers.value = route.params.tickers.toString().split(',');
|
||||
getPriceHistory(tickers.value).then((p: PriceHistory[]) => {
|
||||
loading.value = false;
|
||||
priceHistoryData.value = p;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Reference in a new issue