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:
commit
f40aaa4832
9 changed files with 199 additions and 100 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "ms-python.autopep8"
|
"editor.defaultFormatter": "ms-python.python"
|
||||||
},
|
},
|
||||||
"python.formatting.provider": "none"
|
"python.formatting.provider": "none"
|
||||||
}
|
}
|
24
backend/api/companies.py
Normal file
24
backend/api/companies.py
Normal 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')
|
|
@ -1,102 +1,93 @@
|
||||||
programming_crime_list = [
|
programming_crime_list = [
|
||||||
'AAPL',
|
'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',
|
'ABBV',
|
||||||
'WFC',
|
'ABT',
|
||||||
'NEE',
|
|
||||||
'TMUS',
|
|
||||||
'MCD',
|
|
||||||
'TMO',
|
|
||||||
'ACN',
|
'ACN',
|
||||||
'AVGO',
|
'ADBE',
|
||||||
'NKE',
|
|
||||||
'TGT',
|
|
||||||
'UNP',
|
|
||||||
'HON',
|
|
||||||
'DHR',
|
|
||||||
'ORCL',
|
|
||||||
'LLY',
|
|
||||||
'FIS',
|
|
||||||
'COST',
|
|
||||||
'LOW',
|
|
||||||
'UPS',
|
|
||||||
'AMGN',
|
|
||||||
'MMM',
|
|
||||||
'TXN',
|
|
||||||
'BA',
|
|
||||||
'BMY',
|
|
||||||
'PM',
|
|
||||||
'IBM',
|
|
||||||
'GILD',
|
|
||||||
'ANTM',
|
|
||||||
'LMT',
|
|
||||||
'AMAT',
|
'AMAT',
|
||||||
'SPGI',
|
'AMGN',
|
||||||
'RTX',
|
'AMZN',
|
||||||
'CAT',
|
'ANTM',
|
||||||
'CVS',
|
|
||||||
'MO',
|
|
||||||
'LIN',
|
|
||||||
'GE',
|
|
||||||
'CHTR',
|
|
||||||
'DUK',
|
|
||||||
'GS',
|
|
||||||
'CME',
|
|
||||||
'ISRG',
|
|
||||||
'SPG',
|
|
||||||
'FDX',
|
|
||||||
'BDX',
|
|
||||||
'CCI',
|
|
||||||
'DE',
|
|
||||||
'CCI',
|
|
||||||
'BIDU',
|
|
||||||
'GS',
|
|
||||||
'APD',
|
'APD',
|
||||||
'MCO',
|
'AVGO',
|
||||||
'MMC',
|
'BA',
|
||||||
'NSC',
|
'BAC',
|
||||||
'USB',
|
'BDX',
|
||||||
|
'BIDU',
|
||||||
|
'BMY',
|
||||||
|
'CAT',
|
||||||
|
'CCI',
|
||||||
|
'CHTR',
|
||||||
|
'CMCSA',
|
||||||
|
'CME',
|
||||||
|
'COST',
|
||||||
|
'CRM',
|
||||||
|
'CSCO',
|
||||||
'CSX',
|
'CSX',
|
||||||
'LRCX',
|
'CVS',
|
||||||
'SCHW',
|
'CVX',
|
||||||
'D',
|
'D',
|
||||||
'BDX',
|
'DE',
|
||||||
'EXC',
|
'DHR',
|
||||||
'SO',
|
'DIS',
|
||||||
'DUK',
|
'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'
|
||||||
|
]
|
|
@ -14,7 +14,7 @@
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"vue": "^3.2.0",
|
"vue": "^3.2.0",
|
||||||
"vue-router": "^4.0.0",
|
"vue-router": "^4.0.0",
|
||||||
"vuetify": "^3.0.0",
|
"vuetify": "^3.2.3",
|
||||||
"webfontloader": "^1.0.0"
|
"webfontloader": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
19
stockingly-frontend/src/api/index.ts
Normal file
19
stockingly-frontend/src/api/index.ts
Normal 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())
|
|
@ -10,9 +10,13 @@ import 'vuetify/styles'
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
import { createVuetify } from 'vuetify'
|
import { createVuetify } from 'vuetify'
|
||||||
|
import { VSkeletonLoader } from 'vuetify/labs/VSkeletonLoader'
|
||||||
|
|
||||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||||
export default createVuetify({
|
export default createVuetify({
|
||||||
|
components: {
|
||||||
|
VSkeletonLoader
|
||||||
|
},
|
||||||
theme: {
|
theme: {
|
||||||
defaultTheme: 'light',
|
defaultTheme: 'light',
|
||||||
themes: {
|
themes: {
|
||||||
|
|
|
@ -1,6 +1,59 @@
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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>
|
</script>
|
||||||
|
|
|
@ -1641,10 +1641,10 @@ vue@^3.2.0:
|
||||||
"@vue/server-renderer" "3.2.47"
|
"@vue/server-renderer" "3.2.47"
|
||||||
"@vue/shared" "3.2.47"
|
"@vue/shared" "3.2.47"
|
||||||
|
|
||||||
vuetify@^3.0.0:
|
vuetify@^3.2.3:
|
||||||
version "3.2.2"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.2.2.tgz#a4a39bec15e96b4f9f9be6353e19f156abd91c35"
|
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.2.3.tgz#05767c3d88068654b757d33789b8c249f1d38e00"
|
||||||
integrity sha512-syFfeZVH6dycltqVCx4tDn68fR3r697+Jt7vJW1l3i9a5ClnwpdRBWtE6dt2bjClS2K/VpWYt+rAsLiG7sGU/g==
|
integrity sha512-o7IJm/P5Ttp9ItF1ytQihsLzv4jxIYVfI4Ypkkqc4A7N2MeTmkDOPGbDNUgJ+G1p2upL00LCbc73A9YM8xYVpg==
|
||||||
|
|
||||||
webfontloader@^1.0.0:
|
webfontloader@^1.0.0:
|
||||||
version "1.6.28"
|
version "1.6.28"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
from flask import Flask, jsonify, redirect, url_for, send_from_directory
|
from flask import Flask, jsonify, redirect, url_for, send_from_directory
|
||||||
from flask_cors import CORS
|
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
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
ROOT_DIR: str = os.path.dirname(__file__)
|
||||||
|
|
||||||
# instantiate the app
|
# instantiate the app
|
||||||
app = Flask(__name__, static_url_path='/static', static_folder='stockingly-frontend/dist')
|
app = Flask(__name__, static_url_path='/static', static_folder='stockingly-frontend/dist')
|
||||||
app.config.from_object(__name__)
|
app.config.from_object(__name__)
|
||||||
|
@ -18,6 +21,11 @@ def index():
|
||||||
return redirect(url_for('static', filename='index.html'))
|
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__':
|
if __name__ == '__main__':
|
||||||
build_frontend()
|
build_frontend()
|
||||||
app.run()
|
app.run()
|
||||||
|
|
Reference in a new issue