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:
commit
a5c70003b9
7 changed files with 182 additions and 5 deletions
24
backend/api/assets_debts.py
Normal file
24
backend/api/assets_debts.py
Normal 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')
|
|
@ -30,6 +30,14 @@ export interface PriceHistory {
|
||||||
[ticker: string]: string | number; // really just number
|
[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[]> =>
|
export const getCompanies = (tickers?: string[]): Promise<Company[]> =>
|
||||||
fetch(BACKEND_URL + '/companies' + (tickers ? ('/' + tickers.join('/')) : ''))
|
fetch(BACKEND_URL + '/companies' + (tickers ? ('/' + tickers.join('/')) : ''))
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
|
@ -43,3 +51,6 @@ export const getEmployees = (tickers: string[]): Promise<EmployeeCount[]> =>
|
||||||
|
|
||||||
export const getPriceHistory = (tickers: string[]): Promise<PriceHistory[]> =>
|
export const getPriceHistory = (tickers: string[]): Promise<PriceHistory[]> =>
|
||||||
fetch(BACKEND_URL + '/price_history/' + tickers.join('/').toUpperCase()).then(r => r.json())
|
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())
|
||||||
|
|
|
@ -12,6 +12,5 @@ export const defineLoader = <ApiResult>(apiCall: () => Promise<ApiResult>) => re
|
||||||
alert('Error loading data'); // don't do this for the final product
|
alert('Error loading data'); // don't do this for the final product
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
console.log(this.data, this.loading);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
130
stockingly-frontend/src/components/BalanceSheet.vue
Normal file
130
stockingly-frontend/src/components/BalanceSheet.vue
Normal 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>
|
|
@ -3,7 +3,7 @@
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-item>
|
<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-item>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-skeleton-loader class="chart-loader" v-if="employees.loading" />
|
<v-skeleton-loader class="chart-loader" v-if="employees.loading" />
|
||||||
|
@ -85,7 +85,6 @@ const options = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log(JSON.parse(JSON.stringify(props)));
|
|
||||||
employees.load();
|
employees.load();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -46,7 +46,10 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<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-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
@ -62,6 +65,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30em !important;
|
height: 30em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-row {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -76,6 +83,7 @@ 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';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -154,7 +162,7 @@ const options = computed(() => {
|
||||||
formatter: (params: AgAxisLabelFormatterParams) =>
|
formatter: (params: AgAxisLabelFormatterParams) =>
|
||||||
params?.formatter?.(params.value)
|
params?.formatter?.(params.value)
|
||||||
.replace('k', 'K')
|
.replace('k', 'K')
|
||||||
.replace('G', 'B') ?? ''
|
.replace('G', 'B') ?? '' + '$'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,6 +4,7 @@ from backend.utils.build_frontend import build_frontend
|
||||||
from backend.api.companies import get_companies
|
from backend.api.companies import get_companies
|
||||||
from backend.api.closing_price import get_closing_price_hist
|
from backend.api.closing_price import get_closing_price_hist
|
||||||
from backend.api.employees import get_employees
|
from backend.api.employees import get_employees
|
||||||
|
from backend.api.assets_debts import compare_balance_sheets
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -40,6 +41,11 @@ def price_history(tickers: Optional[str]) -> object:
|
||||||
return jsonify(get_closing_price_hist(tickers.split('/')))
|
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>')
|
@app.route('/companies/logos/<ticker>')
|
||||||
def get_company_logo(ticker: str):
|
def get_company_logo(ticker: str):
|
||||||
logo_dir: str = os.path.join(ROOT_DIR, 'scraper', 'logos', 'logos')
|
logo_dir: str = os.path.join(ROOT_DIR, 'scraper', 'logos', 'logos')
|
||||||
|
|
Reference in a new issue