fixed charts

This commit is contained in:
Claudio Maggioni 2023-05-29 14:41:23 +02:00
parent d3cd94feb0
commit ded132c461
5 changed files with 241 additions and 156 deletions

View file

@ -1,6 +1,4 @@
<template> <template>
<v-row>
<v-col cols="12">
<v-card> <v-card>
<v-card-item> <v-card-item>
<v-card-title>Balance sheet</v-card-title> <v-card-title>Balance sheet</v-card-title>
@ -10,31 +8,20 @@
<ag-charts-vue class="chart" v-else :options="options" /> <ag-charts-vue class="chart" v-else :options="options" />
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col>
</v-row>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BalanceSheet, EmployeeCount, getBalanceSheet } from '@/api'; import { BalanceSheet, getBalanceSheet } from '@/api';
import { defineLoader } from '@/api/loader'; import { defineLoader } from '@/api/loader';
import { AgAxisLabelFormatterParams, time } from 'ag-charts-community'; import { AgAxisLabelFormatterParams } from 'ag-charts-community';
import { DateTime } from 'luxon';
import { roundTo } from 'round-to';
import { onMounted, computed } from 'vue'; import { onMounted, computed } from 'vue';
import { AgChartsVue } from 'ag-charts-vue3'; import { AgChartsVue } from 'ag-charts-vue3';
const renderer = (params: any) => ({
title: params.title,
content: DateTime.fromMillis(params.xValue).year + ': ' + roundTo(params.yValue, 0),
});
const props = defineProps<{ const props = defineProps<{
tickers: string[], tickers: string[],
colors: string[] colors: string[]
}>(); }>();
const getTickerColor = (ticker: string) => props.colors[props.tickers.indexOf(ticker)];
const balanceSheet = defineLoader<BalanceSheet[]>(() => getBalanceSheet(props.tickers)); const balanceSheet = defineLoader<BalanceSheet[]>(() => getBalanceSheet(props.tickers));
const options = computed(() => { const options = computed(() => {
@ -105,6 +92,11 @@ const options = computed(() => {
{ {
type: 'category', type: 'category',
position: 'bottom', position: 'bottom',
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
}, },
{ {
type: 'number', type: 'number',
@ -116,6 +108,11 @@ const options = computed(() => {
.replace('k', 'K') .replace('k', 'K')
.replace('G', 'B') ?? '' .replace('G', 'B') ?? ''
}, },
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
}, },
], ],
legend: { legend: {

View file

@ -1,6 +1,4 @@
<template> <template>
<v-row>
<v-col cols="12">
<v-card> <v-card>
<v-card-item> <v-card-item>
<v-card-title>Employees over time</v-card-title> <v-card-title>Employees over time</v-card-title>
@ -10,8 +8,6 @@
<ag-charts-vue class="chart" v-else :options="options" /> <ag-charts-vue class="chart" v-else :options="options" />
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col>
</v-row>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -28,7 +24,6 @@ const renderer = (params: any) => ({
content: DateTime.fromMillis(params.xValue).year + ': ' + roundTo(params.yValue, 0), content: DateTime.fromMillis(params.xValue).year + ': ' + roundTo(params.yValue, 0),
}); });
const props = defineProps<{ const props = defineProps<{
tickers: string[], tickers: string[],
colors: string[] colors: string[]
@ -71,11 +66,21 @@ const options = computed(() => {
autoRotate: true, autoRotate: true,
autoRotateAngle: 335, autoRotateAngle: 335,
format: '%Y' format: '%Y'
} },
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
}, },
{ {
type: 'number', type: 'number',
position: 'left', position: 'left',
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
}, },
], ],
legend: { legend: {

View file

@ -1,6 +1,4 @@
<template> <template>
<v-row>
<v-col cols="12">
<v-card> <v-card>
<v-card-item> <v-card-item>
<v-card-title>Earnings per Share (EPS)</v-card-title> <v-card-title>Earnings per Share (EPS)</v-card-title>
@ -10,8 +8,6 @@
<ag-charts-vue class="chart" v-else :options="options" /> <ag-charts-vue class="chart" v-else :options="options" />
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col>
</v-row>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -65,11 +61,21 @@ const options = computed(() => {
formatter: (params: any) => { formatter: (params: any) => {
return DateTime.fromMillis(parseInt(params.value)).toFormat('yyyy-MM'); return DateTime.fromMillis(parseInt(params.value)).toFormat('yyyy-MM');
} }
} },
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
}, },
{ {
type: 'number', type: 'number',
position: 'left', position: 'left',
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
}, },
], ],
legend: { legend: {

View file

@ -0,0 +1,176 @@
<template>
<v-card>
<v-card-item>
<v-card-title>Stock price over time</v-card-title>
</v-card-item>
<v-card-text>
<div class="toolbar mb-3">
<v-btn-toggle class="toggles" 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>
<strong class="label">Current price:</strong>
<div class="chips">
<v-chip class="ma-1" v-for="ticker in tickers" :key="ticker" :color="getTickerColor(ticker)" size="large" variant="outlined">
{{ ticker }}: ${{ maxPrice[ticker] }}
</v-chip>
</div>
</div>
<v-skeleton-loader class="chart-loader" v-if="stockPrice.loading" />
<ag-charts-vue class="chart" v-else :options="options" />
</v-card-text>
</v-card>
</template>
<style scoped>
.toolbar {
display: flex;
gap: 10px;
flex-direction: row;
align-items: center;
}
.toolbar .toggles {
flex: 1 0;
}
.toolbar .chips, .toolbar .label {
flex: 0 0;
text-align: center;
}
</style>
<script setup lang="ts">
import { AgChartsVue } from 'ag-charts-vue3';
import { PriceHistory, getPriceHistory } from '@/api';
import { defineLoader } from '@/api/loader';
import { time, AgAxisLabelFormatterParams } from 'ag-charts-community';
import { TimeInterval } from 'ag-charts-community/dist/cjs/es5/util/time/interval';
import { DurationLike, DateTime } from 'luxon';
import { roundTo } from 'round-to';
import { computed, ref, onMounted } from 'vue';
const props = defineProps<{
tickers: string[],
colors: string[]
}>();
type TimeOptions = '1M' | '3M' | '6M' | '1Y' | '5Y' | 'MAX';
const timeToggle = ref<TimeOptions>('1M');
const stockPrice = defineLoader<PriceHistory[]>(() => getPriceHistory(props.tickers));
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() // snap to solar year quarters
}), '%Y-%m'],
'MAX': [undefined, time.year.every(2), '%Y']
};
const getTickerColor = (ticker: string) => props.colors[props.tickers.indexOf(ticker)];
const maxPrice = computed(() => {
const maxVal = Object.fromEntries(props.tickers.map(t => [t, NaN]));
let maxDate = -Infinity;
if (stockPrice.loading) return maxVal;
for (const d of stockPrice.data ?? []) {
if (Date.parse(d.date) > maxDate) {
maxDate = Date.parse(d.date);
for (const ticker in maxVal) {
maxVal[ticker] = d[ticker] as number;
}
}
}
for (const ticker in maxVal) {
maxVal[ticker] = roundTo(maxVal[ticker], 2);
}
return maxVal;
})
const options = computed(() => {
if (stockPrice.loading) 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: stockPrice.data?.map(e => ({ ...e, date: Date.parse(e.date) })),
series: props.tickers.map((t: string) => ({
xKey: 'date',
yKey: t,
yName: t,
stroke: getTickerColor(t),
tooltip: { renderer: renderer },
marker: {
fill: getTickerColor(t),
stroke: getTickerColor(t)
}
})),
axes: [
{
type: 'time',
position: 'bottom',
min: minDate?.toJSDate(),
max: maxDate.toJSDate(),
tick: {
interval: opts[1]
},
label: {
autoRotate: true,
autoRotateAngle: 335,
format: opts[2]
},
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
},
{
type: 'number',
position: 'left',
label: {
format: '$~s',
formatter: (params: AgAxisLabelFormatterParams) =>
params?.formatter?.(params.value)
.replace('k', 'K')
.replace('G', 'B') ?? '' + '$'
},
gridStyle: [{
lineDash: [Infinity]
}, {
lineDash: [Infinity]
}]
},
],
legend: {
position: 'bottom',
},
};
});
onMounted(() => {
stockPrice.load();
});
</script>

View file

@ -25,26 +25,8 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-card> <stock-price :colors="colors" :tickers="tickers" />
<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="stockPrice.loading" />
<ag-charts-vue class="chart" v-else :options="options" />
</v-card-text>
</v-card>
</v-col> </v-col>
</v-row>
<v-row>
<v-col cols="12"> <v-col cols="12">
<employees-chart :colors="colors" :tickers="tickers" /> <employees-chart :colors="colors" :tickers="tickers" />
</v-col> </v-col>
@ -75,112 +57,31 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { AgAxisLabelFormatterParams, time } from 'ag-charts-community'; import { ref, onMounted } from 'vue';
import { AgChartsVue } from 'ag-charts-vue3';
import { ref, onMounted, computed, } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Company, PriceHistory, getCompanies, getPriceHistory } from '@/api'; import { Company, getCompanies } from '@/api';
import { DateTime, DurationLike } from 'luxon';
import { TimeInterval } from 'ag-charts-community/dist/cjs/es5/util/time/interval';
import { roundTo } from 'round-to';
import { defineLoader } from '@/api/loader'; import { defineLoader } from '@/api/loader';
import CompanyCard from '@/components/CompanyCard.vue'; import CompanyCard from '@/components/CompanyCard.vue';
import EmployeesChart from '@/components/EmployeesChart.vue'; import EmployeesChart from '@/components/EmployeesChart.vue';
import BalanceSheet from '@/components/BalanceSheet.vue'; import BalanceSheet from '@/components/BalanceSheet.vue';
import EpsChart from '@/components/EpsChart.vue'; import EpsChart from '@/components/EpsChart.vue';
import StockPrice from '@/components/StockPrice.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const goHome = () => router.push('/'); const goHome = () => router.push('/');
type TimeOptions = '1M' | '3M' | '6M' | '1Y' | '5Y' | 'MAX';
const timeToggle = ref<TimeOptions>('1M');
const companies = defineLoader<Company[]>(() => getCompanies(tickers.value)); const companies = defineLoader<Company[]>(() => getCompanies(tickers.value));
const stockPrice = defineLoader<PriceHistory[]>(() => getPriceHistory(tickers.value));
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() // snap to solar year quarters
}), '%Y-%m'],
'MAX': [undefined, time.year.every(2), '%Y']
};
const tickers = ref<string[]>([]); const tickers = ref<string[]>([]);
const colors = ['#9C27B0', '#D32F2F', '#3F51B5']; const colors = ['#9C27B0', '#D32F2F', '#3F51B5'];
const getTickerColor = (ticker: string) => colors[tickers.value.indexOf(ticker)]; const getTickerColor = (ticker: string) => colors[tickers.value.indexOf(ticker)];
const options = computed(() => {
if (stockPrice.loading) 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: stockPrice.data?.map(e => ({ ...e, date: Date.parse(e.date) })),
series: tickers.value.map((t: string) => ({
xKey: 'date',
yKey: t,
yName: t,
stroke: getTickerColor(t),
tooltip: { renderer: renderer },
marker: {
fill: getTickerColor(t),
stroke: getTickerColor(t)
}
})),
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',
},
};
});
tickers.value = route.params.tickers.toString().split(','); tickers.value = route.params.tickers.toString().split(',');
// load chart data // load chart data
onMounted(() => { onMounted(() => {
stockPrice.load();
companies.load(); companies.load();
}); });
</script> </script>