Merge branch '12-implement-balance-sheet-comparison-chart' into 'master'

Resolve "Implement balance sheet comparison chart"

Closes #12

See merge request usi-si-teaching/msde/2022-2023/visual-analytics-atelier/group-projects/group-1!10
This commit is contained in:
Tommaso Verzegnassi 2023-05-29 08:51:40 +00:00
commit a5c70003b9
7 changed files with 182 additions and 5 deletions

View file

@ -0,0 +1,24 @@
import pandas as pd
import numpy as np
import os
ROOT_PATH: str = os.path.join(os.path.dirname(__file__), '..', '..')
def compare_balance_sheets(ticker_list: list):
df_ret = pd.DataFrame(columns=['total_assets', 'current_assets', 'total_debt', 'current_debt'])
for i, ticker in enumerate(ticker_list):
assets_debt = pd.read_csv(ROOT_PATH + r'/Companies_Data/' + ticker + '_Data/' + ticker + '_balance_sheet_4Y+4Q.csv')
selected_data = assets_debt[['TotalAssets', 'TotalDebt', 'CurrentAssets', 'CurrentDebt']]
df_ret.loc[ticker, 'total_assets'] = selected_data.iloc[0]['TotalAssets']
df_ret.loc[ticker, 'current_assets'] = selected_data.iloc[0]['TotalDebt']
df_ret.loc[ticker, 'total_debt'] = selected_data.iloc[0]['CurrentAssets']
df_ret.loc[ticker, 'current_debt'] = selected_data.iloc[0]['CurrentDebt']
df_ret.loc[:, 'old_assets'] = df_ret['total_assets'].copy() - df_ret['current_assets'].copy()
df_ret.loc[:, 'old_debt'] = df_ret['total_debt'].copy() - df_ret['current_debt'].copy()
df_ret.index.name = 'ticker'
return df_ret.reset_index(drop=False).replace({ np.nan: None }).to_dict('records')

View file

@ -30,6 +30,14 @@ export interface PriceHistory {
[ticker: string]: string | number; // really just number
}
export interface BalanceSheet {
ticker: string;
current_assets: number;
current_debt: number;
total_assets: number;
total_debt: number;
}
export const getCompanies = (tickers?: string[]): Promise<Company[]> =>
fetch(BACKEND_URL + '/companies' + (tickers ? ('/' + tickers.join('/')) : ''))
.then(r => r.json())
@ -43,3 +51,6 @@ export const getEmployees = (tickers: string[]): Promise<EmployeeCount[]> =>
export const getPriceHistory = (tickers: string[]): Promise<PriceHistory[]> =>
fetch(BACKEND_URL + '/price_history/' + tickers.join('/').toUpperCase()).then(r => r.json())
export const getBalanceSheet = (tickers: string[]): Promise<BalanceSheet[]> =>
fetch(BACKEND_URL + '/assets_debts/' + tickers.join('/')).then(r=>r.json())

View file

@ -12,6 +12,5 @@ export const defineLoader = <ApiResult>(apiCall: () => Promise<ApiResult>) => re
alert('Error loading data'); // don't do this for the final product
}
this.loading = false;
console.log(this.data, this.loading);
}
})

View file

@ -0,0 +1,130 @@
<template>
<v-row>
<v-col cols="12">
<v-card>
<v-card-item>
<v-card-title>Balance sheet</v-card-title>
</v-card-item>
<v-card-text>
<v-skeleton-loader class="chart-loader" v-if="balanceSheet.loading" />
<ag-charts-vue class="chart" v-else :options="options" />
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import { BalanceSheet, EmployeeCount, getBalanceSheet } from '@/api';
import { defineLoader } from '@/api/loader';
import { AgAxisLabelFormatterParams, time } from 'ag-charts-community';
import { DateTime } from 'luxon';
import { roundTo } from 'round-to';
import { onMounted, computed } from 'vue';
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<{
tickers: string[],
colors: string[]
}>();
const getTickerColor = (ticker: string) => props.colors[props.tickers.indexOf(ticker)];
const balanceSheet = defineLoader<BalanceSheet[]>(() => getBalanceSheet(props.tickers));
const options = computed(() => {
if (balanceSheet.loading) return null;
const currencyFormat = (num: number) => {
return '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')
}
const renderer = (params: any) => {
return {
title: params.title,
content: params.xValue + ': ' + currencyFormat(params.yKey.startsWith('old') ?
(params.datum[params.yKey.replace('old', 'total')]) :
(params.yValue)),
}
};
return {
theme: 'ag-material',
data: balanceSheet.data,
series: [
{
type: 'column',
xKey: 'ticker',
yKey: 'current_assets',
yName: 'Current Assets',
stackGroup: 'Assets',
fill: '#004D40',
stroke: '#004D40'
},
{
type: 'column',
xKey: 'ticker',
yKey: 'old_assets',
yName: 'Total Assets',
stackGroup: 'Assets',
fill: '#4DB6AC',
stroke: '#4DB6AC'
},
{
type: 'column',
xKey: 'ticker',
yKey: 'current_debt',
yName: 'Current Debt',
stackGroup: 'Debt',
fill: '#880E4F',
stroke: '#880E4F'
},
{
type: 'column',
xKey: 'ticker',
yKey: 'old_debt',
yName: 'Total Debt',
stackGroup: 'Debt',
fill: '#F06292',
stroke: '#F06292'
}
].map(e => ({
...e,
highlightStyle: {
item: { fillOpacity: 0 },
series: { enabled: false }
},
tooltip: { renderer: renderer },
})),
axes: [
{
type: 'category',
position: 'bottom',
},
{
type: 'number',
position: 'left',
label: {
format: '$~s',
formatter: (params: AgAxisLabelFormatterParams) =>
params?.formatter?.(params.value)
.replace('k', 'K')
.replace('G', 'B') ?? ''
},
},
],
legend: {
position: 'bottom',
},
};
});
onMounted(() => {
balanceSheet.load();
});
</script>

View file

@ -3,7 +3,7 @@
<v-col cols="12">
<v-card>
<v-card-item>
<v-card-title>Stock price over time</v-card-title>
<v-card-title>Employees over time</v-card-title>
</v-card-item>
<v-card-text>
<v-skeleton-loader class="chart-loader" v-if="employees.loading" />
@ -85,7 +85,6 @@ const options = computed(() => {
});
onMounted(() => {
console.log(JSON.parse(JSON.stringify(props)));
employees.load();
});
</script>

View file

@ -46,7 +46,10 @@
</v-row>
<v-row>
<v-col cols="12">
<EmployeesChart :colors="colors" :tickers="tickers"></EmployeesChart>
<employees-chart :colors="colors" :tickers="tickers" />
</v-col>
<v-col cols="12">
<balance-sheet :colors="colors" :tickers="tickers" />
</v-col>
</v-row>
</v-container>
@ -62,6 +65,10 @@
width: 100%;
height: 30em !important;
}
.v-row {
width: 100% !important;
}
</style>
<script setup lang="ts">
@ -76,6 +83,7 @@ import { roundTo } from 'round-to';
import { defineLoader } from '@/api/loader';
import CompanyCard from '@/components/CompanyCard.vue';
import EmployeesChart from '@/components/EmployeesChart.vue';
import BalanceSheet from '@/components/BalanceSheet.vue';
const route = useRoute();
const router = useRouter();
@ -154,7 +162,7 @@ const options = computed(() => {
formatter: (params: AgAxisLabelFormatterParams) =>
params?.formatter?.(params.value)
.replace('k', 'K')
.replace('G', 'B') ?? ''
.replace('G', 'B') ?? '' + '$'
},
},
],

View file

@ -4,6 +4,7 @@ from backend.utils.build_frontend import build_frontend
from backend.api.companies import get_companies
from backend.api.closing_price import get_closing_price_hist
from backend.api.employees import get_employees
from backend.api.assets_debts import compare_balance_sheets
import os
import sys
import subprocess
@ -40,6 +41,11 @@ def price_history(tickers: Optional[str]) -> object:
return jsonify(get_closing_price_hist(tickers.split('/')))
@app.route('/assets_debts/<path:tickers>', methods=['GET'])
def assets_debts(tickers: Optional[str]) -> object:
return jsonify(compare_balance_sheets(tickers.split('/')))
@app.route('/companies/logos/<ticker>')
def get_company_logo(ticker: str):
logo_dir: str = os.path.join(ROOT_DIR, 'scraper', 'logos', 'logos')