Employees graph done
This commit is contained in:
parent
373090144c
commit
709440c1dd
6 changed files with 137 additions and 9 deletions
21
backend/api/employees.py
Normal file
21
backend/api/employees.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
ROOT_PATH: str = os.path.join(os.path.dirname(__file__), '..', '..')
|
||||||
|
|
||||||
|
|
||||||
|
def get_employees(tickers: list[str]) -> list[dict]:
|
||||||
|
employees_df = pd.read_csv(ROOT_PATH + '/Elaborated_Data/employees_over_time.csv', index_col=[0])
|
||||||
|
|
||||||
|
s_tickers = pd.Series(tickers)
|
||||||
|
employees_df = employees_df.loc[employees_df['Ticker'].isin(s_tickers), :]
|
||||||
|
|
||||||
|
dates = [str(date) + '-01-01' for date in range(2012, 2023 + 1)]
|
||||||
|
|
||||||
|
for i, date in enumerate(dates):
|
||||||
|
employees_df[date] = employees_df['Employees_over_time'].copy().apply(lambda x: int(eval(str(x))[i]))
|
||||||
|
|
||||||
|
employees_df = pd.melt(employees_df, id_vars=['Ticker'], value_vars=dates, var_name='year').reset_index(drop=True)
|
||||||
|
employees_df = employees_df.pivot(index='year', columns='Ticker', values='value').reset_index(drop=False)
|
||||||
|
return employees_df.replace({ np.nan: None }).to_dict('records')
|
|
@ -20,6 +20,11 @@ export interface Company {
|
||||||
'Past Performance': number;
|
'Past Performance': number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EmployeeCount {
|
||||||
|
year: string;
|
||||||
|
[ticker: string]: string | number; // really just number
|
||||||
|
}
|
||||||
|
|
||||||
export interface PriceHistory {
|
export interface PriceHistory {
|
||||||
date: string;
|
date: string;
|
||||||
[ticker: string]: string | number; // really just number
|
[ticker: string]: string | number; // really just number
|
||||||
|
@ -33,5 +38,8 @@ export const getCompanies = (tickers?: string[]): Promise<Company[]> =>
|
||||||
logoSrc: `${BACKEND_URL}/companies/logos/${e.ticker}`
|
logoSrc: `${BACKEND_URL}/companies/logos/${e.ticker}`
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
export const getEmployees = (tickers: string[]): Promise<EmployeeCount[]> =>
|
||||||
|
fetch(BACKEND_URL + '/employees/' + tickers.join('/').toUpperCase()).then(r => r.json())
|
||||||
|
|
||||||
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())
|
|
@ -15,9 +15,6 @@
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
|
|
||||||
<v-card-text class="flex-0" v-if="!props.minimal">
|
<v-card-text class="flex-0" v-if="!props.minimal">
|
||||||
<p class="text--primary">
|
|
||||||
{{ company.description }}
|
|
||||||
</p>
|
|
||||||
<div class="pt-2 pb-2" v-for="m in metrics" :key="m.title">
|
<div class="pt-2 pb-2" v-for="m in metrics" :key="m.title">
|
||||||
<div class="d-inline-flex justify-space-between" style="width: 100%">
|
<div class="d-inline-flex justify-space-between" style="width: 100%">
|
||||||
<strong>{{ m.title }}</strong>
|
<strong>{{ m.title }}</strong>
|
||||||
|
@ -120,7 +117,6 @@ const metricsData = reactive<Metric[]>([
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
maxValue: 200,
|
maxValue: 200,
|
||||||
value: c => c['Estimated Growth'],
|
value: c => c['Estimated Growth'],
|
||||||
symbol: ' %',
|
|
||||||
scale: [
|
scale: [
|
||||||
{ gt: 8, color: COLORS.good },
|
{ gt: 8, color: COLORS.good },
|
||||||
{ gt: 0, color: COLORS.warning },
|
{ gt: 0, color: COLORS.warning },
|
||||||
|
@ -132,7 +128,6 @@ const metricsData = reactive<Metric[]>([
|
||||||
minValue: -100,
|
minValue: -100,
|
||||||
maxValue: 200,
|
maxValue: 200,
|
||||||
value: c => c['Past Performance'],
|
value: c => c['Past Performance'],
|
||||||
symbol: ' %',
|
|
||||||
scale: [
|
scale: [
|
||||||
{ gt: 5, color: COLORS.good },
|
{ gt: 5, color: COLORS.good },
|
||||||
{ gt: -5, color: COLORS.warning },
|
{ gt: -5, color: COLORS.warning },
|
||||||
|
@ -149,7 +144,7 @@ type ExtendedMetric = Metric & {
|
||||||
const metrics = computed<ExtendedMetric[]>(() => metricsData.map(e => ({
|
const metrics = computed<ExtendedMetric[]>(() => metricsData.map(e => ({
|
||||||
...e,
|
...e,
|
||||||
percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue),
|
percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue),
|
||||||
value: (c: Company) => roundTo(e.value(c), e.decimals ?? 2),
|
value: (c: Company) => roundTo(e.value(c), e.decimals ?? 0),
|
||||||
color: (c: Company) => {
|
color: (c: Company) => {
|
||||||
const value = e.value(c);
|
const value = e.value(c);
|
||||||
for (const s of e.scale) {
|
for (const s of e.scale) {
|
||||||
|
|
91
stockingly-frontend/src/components/EmployeesChart.vue
Normal file
91
stockingly-frontend/src/components/EmployeesChart.vue
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<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-skeleton-loader class="chart-loader" v-if="employees.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 { EmployeeCount, getEmployees } from '@/api';
|
||||||
|
import { defineLoader } from '@/api/loader';
|
||||||
|
import { 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 employees = defineLoader<EmployeeCount[]>(() => getEmployees(props.tickers));
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
if (employees.loading) return null;
|
||||||
|
|
||||||
|
const minDate = DateTime.utc(2012);
|
||||||
|
const maxDate = DateTime.utc(2023);
|
||||||
|
|
||||||
|
return {
|
||||||
|
theme: 'ag-material',
|
||||||
|
data: employees.data?.map(e => ({ ...e, year: Date.parse(e.year) })),
|
||||||
|
series: props.tickers.map((t: string) => ({
|
||||||
|
xKey: 'year',
|
||||||
|
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: time.year
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateAngle: 335,
|
||||||
|
format: '%Y'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
position: 'left',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(JSON.parse(JSON.stringify(props)));
|
||||||
|
employees.load();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -44,6 +44,11 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<EmployeesChart :colors="colors" :tickers="tickers"></EmployeesChart>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -62,7 +67,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AgAxisLabelFormatterParams, time } from 'ag-charts-community';
|
import { AgAxisLabelFormatterParams, time } from 'ag-charts-community';
|
||||||
import { AgChartsVue } from 'ag-charts-vue3';
|
import { AgChartsVue } from 'ag-charts-vue3';
|
||||||
import { ref, onMounted, computed } from 'vue';
|
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, PriceHistory, getCompanies, getPriceHistory } from '@/api';
|
||||||
import { DateTime, DurationLike } from 'luxon';
|
import { DateTime, DurationLike } from 'luxon';
|
||||||
|
@ -70,6 +75,7 @@ import { TimeInterval } from 'ag-charts-community/dist/cjs/es5/util/time/interva
|
||||||
import { roundTo } from 'round-to';
|
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';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -158,9 +164,10 @@ const options = computed(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tickers.value = route.params.tickers.toString().split(',');
|
||||||
|
|
||||||
// load chart data
|
// load chart data
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
tickers.value = route.params.tickers.toString().split(',');
|
|
||||||
stockPrice.load();
|
stockPrice.load();
|
||||||
companies.load();
|
companies.load();
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ from flask_cors import CORS
|
||||||
from backend.utils.build_frontend import build_frontend
|
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
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -29,6 +30,11 @@ def companies(tickers: Optional[str] = None) -> object:
|
||||||
return jsonify(get_companies(None if tickers is None else tickers.split('/')))
|
return jsonify(get_companies(None if tickers is None else tickers.split('/')))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/employees/<path:tickers>', methods=['GET'])
|
||||||
|
def employees(tickers: Optional[str]) -> object:
|
||||||
|
return jsonify(get_employees(tickers.split('/')))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/price_history/<path:tickers>', methods=['GET'])
|
@app.route('/price_history/<path:tickers>', methods=['GET'])
|
||||||
def price_history(tickers: Optional[str]) -> object:
|
def price_history(tickers: Optional[str]) -> object:
|
||||||
return jsonify(get_closing_price_hist(tickers.split('/')))
|
return jsonify(get_closing_price_hist(tickers.split('/')))
|
||||||
|
|
Reference in a new issue