Merge branch '6-show-list-of-companies-in-web-app' into 'master'

Resolve "Show list of companies in web app"

Closes #6

See merge request usi-si-teaching/msde/2022-2023/visual-analytics-atelier/group-projects/group-1!4
This commit is contained in:
Claudio Maggioni 2023-05-10 08:34:03 +00:00
commit f40aaa4832
9 changed files with 199 additions and 100 deletions

View file

@ -1,6 +1,6 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8"
"editor.defaultFormatter": "ms-python.python"
},
"python.formatting.provider": "none"
}

24
backend/api/companies.py Normal file
View file

@ -0,0 +1,24 @@
import os
import pandas as pd
import numpy as np
from scraper.top100_extractor import programming_crime_list
COMPANIES_CSV_PATH: str = 'scraper/companies.csv'
def non_nan(a: list[any]) -> list[any]:
return list(filter(lambda a: type(a) == str or not np.isnan(a), a))
def get_companies(root_dir: str) -> list[dict]:
"""
reads the companies.csv file and returns it as a JSON-ifiable object
to return to the frontend.
"""
df = pd.read_csv(os.path.join(root_dir, COMPANIES_CSV_PATH), index_col='ticker')
tickers = pd.Series(programming_crime_list)
df = df.loc[df.index.isin(tickers), :]
df['tags'] = df[['tag 1', 'tag 2', 'tag 3']].values.tolist()
df['tags'] = df['tags'].apply(non_nan)
del df['tag 1']
del df['tag 2']
del df['tag 3']
return df.reset_index().replace({ np.nan: None }).to_dict('records')

View file

@ -1,102 +1,93 @@
programming_crime_list = [
'AAPL',
'MSFT',
'AMZN',
'GOOGL',
'META',
'BRK-A',
'TSLA',
'JPM',
'JNJ',
'V',
'PG',
'MA',
'NVDA',
'UNH',
'HD',
'BAC',
'DIS',
'PYPL',
'KO',
'INTC',
'VZ',
'ADBE',
'NFLX',
'CRM',
'PFE',
'MRK',
'CMCSA',
'T',
'ABT',
'PEP',
'XOM',
'CVX',
'WMT',
'CSCO',
'MDT',
'ABBV',
'WFC',
'NEE',
'TMUS',
'MCD',
'TMO',
'ABT',
'ACN',
'AVGO',
'NKE',
'TGT',
'UNP',
'HON',
'DHR',
'ORCL',
'LLY',
'FIS',
'COST',
'LOW',
'UPS',
'AMGN',
'MMM',
'TXN',
'BA',
'BMY',
'PM',
'IBM',
'GILD',
'ANTM',
'LMT',
'ADBE',
'AMAT',
'SPGI',
'RTX',
'CAT',
'CVS',
'MO',
'LIN',
'GE',
'CHTR',
'DUK',
'GS',
'CME',
'ISRG',
'SPG',
'FDX',
'BDX',
'CCI',
'DE',
'CCI',
'BIDU',
'GS',
'AMGN',
'AMZN',
'ANTM',
'APD',
'MCO',
'MMC',
'NSC',
'USB',
'AVGO',
'BA',
'BAC',
'BDX',
'BIDU',
'BMY',
'CAT',
'CCI',
'CHTR',
'CMCSA',
'CME',
'COST',
'CRM',
'CSCO',
'CSX',
'LRCX',
'SCHW',
'CVS',
'CVX',
'D',
'BDX',
'EXC',
'SO',
'DE',
'DHR',
'DIS',
'DUK',
'BDX',
'EXC'
]
'EXC',
'FDX',
'FIS',
'GE',
'GILD',
'GOOGL',
'GS',
'HD',
'HON',
'IBM',
'INTC',
'ISRG',
'JNJ',
'JPM',
'KO',
'LIN',
'LLY',
'LMT',
'LOW',
'LRCX',
'MA',
'MCD',
'MCO',
'MDT',
'MMC',
'MMM',
'MO',
'MRK',
'MSFT',
'NEE',
'NFLX',
'NKE',
'NSC',
'NVDA',
'ORCL',
'PEP',
'PFE',
'PG',
'PM',
'PYPL',
'SCHW',
'SO',
'SPG',
'SPGI',
'T',
'TGT',
'TMO',
'TMUS',
'TSLA',
'TXN',
'UNH',
'UNP',
'UPS',
'USB',
'V',
'VZ',
'WFC',
'WMT',
'XOM'
]

View file

@ -14,7 +14,7 @@
"roboto-fontface": "*",
"vue": "^3.2.0",
"vue-router": "^4.0.0",
"vuetify": "^3.0.0",
"vuetify": "^3.2.3",
"webfontloader": "^1.0.0"
},
"devDependencies": {

View file

@ -0,0 +1,19 @@
const BACKEND_URL = "http://localhost:5000";
export interface Company {
ceo: string;
"company name": string;
description: string;
exchange: string;
industry: string;
logo: string;
"market cap": number;
sector: string;
"short name": string;
tags: string[];
ticker: string;
website: string;
}
export const getCompanies = (): Promise<Company[]> =>
fetch(BACKEND_URL + '/companies').then(r => r.json())

View file

@ -10,9 +10,13 @@ import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
import { VSkeletonLoader } from 'vuetify/labs/VSkeletonLoader'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
components: {
VSkeletonLoader
},
theme: {
defaultTheme: 'light',
themes: {

View file

@ -1,6 +1,59 @@
<template>
<marquee><h1>Viva Luciano Malusa</h1></marquee>
<v-container class="fill-height">
<v-row>
<template v-if="loading" v-for="i in Array.from({ length: 10 }, (_, i) => i)" :key="i">
<v-col cols="12" md="6" lg="4">
<v-skeleton-loader class="mx-auto" type="card"></v-skeleton-loader>
</v-col>
</template>
<template v-else v-for="company in companies" :key="company.ticker">
<v-col cols="12" md="6" lg="4">
<v-card class="ma-1 fill-height">
<v-card-item>
<v-card-title>{{ company['short name'] }}</v-card-title>
<v-card-subtitle>{{ company['company name'] }}</v-card-subtitle>
</v-card-item>
<v-card-text>
{{ company.description }}
</v-card-text>
<div class="px-4">
<v-chip label color="pink" class="ma-1">
<v-icon start icon="mdi-chart-timeline-variant"></v-icon>
{{ company.ticker }}
</v-chip>
<v-chip label color="green" class="ma-1">
<v-icon start icon="mdi-currency-usd"></v-icon>
{{ formatCurrency(company['market cap']) }}
</v-chip>
<template v-for="tag in company.tags" :key="tag">
<v-chip label class="ma-1">{{ tag }}</v-chip>
</template>
</div>
</v-card>
</v-col>
</template>
</v-row>
</v-container>
</template>
<script lang="ts" setup>
import { getCompanies, Company } from '@/api';
import { ref, reactive } from 'vue';
const loading = ref(true);
const companies = reactive<Company[]>([]);
getCompanies().then(cs => {
loading.value = false;
companies.push(...cs);
});
const formatCurrency = (d: number) => {
if (d < 1000) return `${d}`;
if (d < 1_000_000) return `${Math.round(d / 1000)} K`;
if (d < 1_000_000_000) return `${Math.round(d / 1_000_000)} M`
return `${Math.round(d / 1_000_000_000)} B`
}
</script>

View file

@ -1641,10 +1641,10 @@ vue@^3.2.0:
"@vue/server-renderer" "3.2.47"
"@vue/shared" "3.2.47"
vuetify@^3.0.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.2.2.tgz#a4a39bec15e96b4f9f9be6353e19f156abd91c35"
integrity sha512-syFfeZVH6dycltqVCx4tDn68fR3r697+Jt7vJW1l3i9a5ClnwpdRBWtE6dt2bjClS2K/VpWYt+rAsLiG7sGU/g==
vuetify@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.2.3.tgz#05767c3d88068654b757d33789b8c249f1d38e00"
integrity sha512-o7IJm/P5Ttp9ItF1ytQihsLzv4jxIYVfI4Ypkkqc4A7N2MeTmkDOPGbDNUgJ+G1p2upL00LCbc73A9YM8xYVpg==
webfontloader@^1.0.0:
version "1.6.28"

View file

@ -1,10 +1,13 @@
from flask import Flask, jsonify, redirect, url_for, send_from_directory
from flask_cors import CORS
from backend.utils.build_frontend import build_frontend
from backend.api.companies import get_companies
import os
import subprocess
ROOT_DIR: str = os.path.dirname(__file__)
# instantiate the app
app = Flask(__name__, static_url_path='/static', static_folder='stockingly-frontend/dist')
app.config.from_object(__name__)
@ -18,6 +21,11 @@ def index():
return redirect(url_for('static', filename='index.html'))
@app.route('/companies', methods=['GET'])
def companies() -> object:
return jsonify(get_companies(ROOT_DIR))
if __name__ == '__main__':
build_frontend()
app.run()