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",
|
"core-js": "^3.29.0",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
|
"luxon": "^3.3.0",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"round-to": "^6.0.0",
|
"round-to": "^6.0.0",
|
||||||
"vue": "^3.2.0",
|
"vue": "^3.2.0",
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "^7.21.4",
|
"@babel/types": "^7.21.4",
|
||||||
"@types/debounce": "^1.2.1",
|
"@types/debounce": "^1.2.1",
|
||||||
|
"@types/luxon": "^3.3.0",
|
||||||
"@types/node": "^18.15.0",
|
"@types/node": "^18.15.0",
|
||||||
"@types/webfontloader": "^1.6.35",
|
"@types/webfontloader": "^1.6.35",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
|
|
@ -8,44 +8,142 @@
|
||||||
</v-app-bar-title>
|
</v-app-bar-title>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-container class="fill-height">
|
<v-container class="fill-height">
|
||||||
<h1>Stock price over time</h1>
|
<v-row>
|
||||||
<ag-charts-vue v-if="options !== null" :options="options" style="width: 100%; height: 30em" />
|
<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>
|
</v-container>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
|
import { AgAxisLabelFormatterParams, AgSeriesTooltipRendererParams, time } from 'ag-charts-community';
|
||||||
import { AgChartsVue } from 'ag-charts-vue3';
|
import { AgChartsVue } from 'ag-charts-vue3';
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { PriceHistory, getPriceHistory } from '@/api';
|
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 route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const goHome = () => router.push('/');
|
const goHome = () => router.push('/');
|
||||||
|
|
||||||
const options = ref<object | null>(null);
|
type TimeOptions = '1M' | '3M' | '6M' | '1Y' | '5Y' | 'MAX';
|
||||||
|
|
||||||
// load chart data
|
const timeToggle = ref<TimeOptions>('1M');
|
||||||
onMounted(() => {
|
|
||||||
const tickers = route.params.tickers.toString().split(',');
|
const optToDuration: Record<TimeOptions, [DurationLike | undefined, TimeInterval, string]> = {
|
||||||
console.log(tickers);
|
'1M': [{ months: 1 }, time.day, '%d'],
|
||||||
getPriceHistory(tickers).then((p: PriceHistory[]) => {
|
'3M': [{ months: 3 }, time.friday, '%m-%d'],
|
||||||
options.value = {
|
'6M': [{ months: 6 }, time.friday.every(2), '%m-%d'],
|
||||||
data: p.map(e => ({ ...e, date: Date.parse(e.date) })),
|
'1Y': [{ years: 1 }, time.month, '%Y-%m'],
|
||||||
series: tickers.map((t: string) => ({ xKey: 'date', yKey: t })),
|
'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: [
|
axes: [
|
||||||
{
|
{
|
||||||
type: 'time',
|
type: 'time',
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
|
min: minDate?.toJSDate(),
|
||||||
|
max: maxDate.toJSDate(),
|
||||||
|
tick: {
|
||||||
|
interval: opts[1]
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateAngle: 335,
|
||||||
|
format: opts[2]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
position: 'left',
|
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"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
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":
|
"@types/node@^18.15.0":
|
||||||
version "18.16.3"
|
version "18.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01"
|
||||||
|
@ -1207,6 +1212,11 @@ lru-cache@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
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:
|
magic-string@^0.25.7:
|
||||||
version "0.25.9"
|
version "0.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||||
|
|
Reference in a new issue