Visualize financials (#206)

Adds the financial graph to the company page. The graph is only
available for companies with existing financial data.
This commit is contained in:
KM-R
2023-10-14 17:08:34 +02:00
committed by GitHub
parent c8d3c7395b
commit 9f7d714403
10 changed files with 723 additions and 250 deletions

688
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ from aki_prj23_transparenzregister.utils.sql import connector
app = Dash(
__name__,
use_pages=True,
suppress_callback_exceptions=True,
external_stylesheets=[dbc.icons.BOOTSTRAP],
title="Transparenzregister",
) # use dbc for icons

View File

@ -17,7 +17,7 @@
float: left;
width: 100%;
background-color: white;
margin-top: 20px;
margin-top: 10px;
margin-right: 2%;
}

View File

@ -1,6 +1,6 @@
.tabs {
float: left;
margin-top: 20px;
margin-top: 10px;
border: 1px solid;
width: 100%;
}
@ -21,3 +21,31 @@
background-color: var(--paynes-gray) !important;
font-weight: bold !important;
}
.choose-metric {
margin: 20px;
width: 100%;
}
.choose-metric .metrics-title {
color: var(--raisin-black);
padding-right: 30px;
vertical-align:middle;
display: inline-block;
}
.choose-metric .finance-dropdown {
width: 400px;
vertical-align: middle;
display: inline-block;
}
.metrics-graph .metrics-graph-response {
margin: 20px;
display: inline-block;
vertical-align: middle;
}
.metrics-graph .metrics-disclaimer {
margin: 20px;
}

View File

@ -1,7 +1,9 @@
"""Dash elements."""
import pandas as pd
from dash import dash_table, dcc, html
from sqlalchemy.orm import Session
from aki_prj23_transparenzregister.ui import finance_elements
@ -121,12 +123,12 @@ def create_company_stats(selected_company_data: pd.Series) -> html:
)
def create_tabs(selected_company_id: int, selected_finance_df: pd.DataFrame) -> html:
def create_tabs(session: Session, selected_company_id: int) -> html:
"""Create tabs for more company information.
Args:
session: A session connecting to the database.
selected_company_id: Id of the chosen company in the dropdown.
selected_finance_df: A dataframe containing all available finance information of the companies.
Returns:
The html div to create the tabs of the company page.
@ -143,7 +145,11 @@ def create_tabs(selected_company_id: int, selected_finance_df: pd.DataFrame) ->
value="tab-1",
className="tab-style",
selected_className="selected-tab-style",
children=[kennzahlen_layout(selected_finance_df)],
children=[
finance_elements.financial_metrics_layout(
session, selected_company_id
)
],
),
dcc.Tab(
label="Beteiligte Personen",
@ -171,26 +177,6 @@ def create_tabs(selected_company_id: int, selected_finance_df: pd.DataFrame) ->
)
def kennzahlen_layout(selected_finance_df: pd.DataFrame) -> html:
"""Create metrics tab.
Args:
selected_finance_df: A dataframe containing all available finance information of the companies.
Returns:
The html div to create the metrics tab of the company page.
"""
return html.Div(
[
dcc.Graph(
figure=finance_elements.financials_figure(
selected_finance_df, "annual_finance_statement_ebit"
)
)
]
)
def network_layout(selected_company_id: int) -> html:
"""Create network tab.

View File

@ -64,6 +64,32 @@ def get_finance_data(session: Session) -> pd.DataFrame:
return pd.read_sql(str(query_finance), engine)
def get_finance_data_of_one_company(session: Session, company_id: int) -> pd.DataFrame:
"""Collects all available finance data of one company.
Args:
session: A session connecting to the database.
company_id: Id of the company.
Returns:
A dataframe containing all financial data of the selected company.
"""
annual_finance_data = (
session.query(entities.AnnualFinanceStatement)
.filter(entities.AnnualFinanceStatement.company_id == company_id)
.all()
)
engine = session.bind
if not isinstance(engine, Engine):
raise TypeError
data = [row.__dict__ for row in annual_finance_data]
if "_sa_instance_state" not in pd.DataFrame(data).columns:
return pd.DataFrame(data)
return pd.DataFrame(data).drop(columns=["_sa_instance_state"])
@cached( # type: ignore
cache=TTLCache(maxsize=1, ttl=300),
key=lambda session: 0 if session is None else str(session.bind),
@ -82,7 +108,7 @@ def get_options(session: Session | None) -> dict[int, str]:
persons = get_person_data(session).rename("p_{}".format)
persons["person_lastname_firstname"] = (
persons["person_surname"] + ", " + persons["person_name"]
persons["person_lastname"] + ", " + persons["person_firstname"]
)
persons_options = persons["person_lastname_firstname"]
companies = get_company_data(session).rename("c_{}".format)

View File

@ -1,7 +1,13 @@
"""Finance elements for Dash."""
from io import StringIO
import pandas as pd
import plotly.graph_objs as go
from dash import Input, Output, State, callback, dcc, html
from sqlalchemy.orm import Session
from aki_prj23_transparenzregister.ui import data_elements
COLORS = {
"light": "#edefef",
@ -12,6 +18,126 @@ COLORS = {
"raisin-black": "#2e2c2f",
}
METRICS = {
"revenue": "Umsatz",
"net_income": "Jahresüberschuss",
"ebit": "EBIT",
"ebitda": "EBITDA",
"gross_profit": "Bruttogewinn",
"operating_profit": "Betriebsgewinn",
"assets": "Bilanzsumme",
"liabilities": "Gesamtverbindlichkeiten",
"equity": "Eigenkapital",
"current_assets": "Umlaufvermögen",
"current_liabilities": "Kurzfristige Verbindlichkeiten",
"long_term_debt": "Langfristige Verbindlichkeiten",
"short_term_debt": "Kurzfristige Verbindlichkeiten",
"cash_and_cash_equivalents": "Barmittel",
"dividends": "Dividende",
"cash_flow": "Cash Flow",
}
def financial_metrics_layout(session: Session, selected_company_id: int) -> html:
"""Create metrics tab.
Args:
session: A session connecting to the database.
selected_company_id: Id of the chosen company in the dropdown.
Returns:
The html div to create the metrics tab of the company page.
"""
finance_df = data_elements.get_finance_data_of_one_company(
session, selected_company_id
)
available_metrics = METRICS.copy()
for column in finance_df.columns:
if finance_df[column].count() == 0 and column in available_metrics:
del available_metrics[column]
if finance_df.empty:
return html.Div(
className="choose-metric",
children=[
html.H3(
className="metrics-title",
children=[
"Für dieses Unternehmen stehen leider keine Finanzdaten zur Verfügung."
],
)
],
)
return html.Div(
[
html.Div(
className="choose-metric",
children=[
html.H3(
className="metrics-title",
children=["Wählen Sie eine Kennzahl:"],
),
html.Div(
className="finance-dropdown",
children=[
dcc.Dropdown(
id="metrics-dropdown",
options=available_metrics,
placeholder="Kennzahl",
),
],
),
],
),
dcc.Store(
id="store-finance-df", data=finance_df.to_json(date_format="iso")
),
html.Div(
className="metrics-graph",
id="finance-fig",
),
]
)
@callback(
Output("finance-fig", "children"),
Input("metrics-dropdown", "value"),
[State("store-finance-df", "data")],
prevent_initial_call=True,
)
def update_figure(value: str, data: str) -> html:
"""Update graph after selecting from dropdown.
Args:
value: Value of the dropdown selection.
data: Stored financial data of the selected company.
Returns:
Finance graph.
"""
if value is None:
return ""
finance_df = pd.read_json(StringIO(data))
if finance_df[value].count() <= 2: # noqa: PLR2004
return html.H3(
className="metrics-graph-response",
children=[
"Für diese Auswahl stehen leider nicht genügend Daten zur Verfügung."
],
)
return (
dcc.Graph(figure=financials_figure(finance_df, value)),
html.Div(
className="metrics-disclaimer",
children=[
"Die hier angezeigten Daten stammen aus öffentlichen Quellen und wurden vollautomatisch verarbeitet, analysiert und visualisiert. Die Daten können teilweise oder weitgehend fehlerhaft sein."
],
),
)
def financials_figure(selected_finance_df: pd.DataFrame, metric: str) -> go.Figure:
"""Creates plotly line chart for a specific company and a metric.
@ -28,7 +154,7 @@ def financials_figure(selected_finance_df: pd.DataFrame, metric: str) -> go.Figu
# add trace for company 1
fig_line.add_trace(
go.Scatter(
x=selected_finance_df["annual_finance_statement_date"],
x=selected_finance_df["date"],
y=selected_finance_df[metric],
line_color=COLORS["raisin-black"],
marker_color=COLORS["raisin-black"],
@ -36,9 +162,8 @@ def financials_figure(selected_finance_df: pd.DataFrame, metric: str) -> go.Figu
)
# set title and labels
fig_line.update_layout(
title=metric,
xaxis_title="Jahr",
yaxis_title="in Mio.€",
title="Entwicklungsverlauf: " + METRICS[metric],
yaxis_title=f"{METRICS[metric]} in Euro (€)",
plot_bgcolor=COLORS["light"],
)
return fig_line

View File

@ -32,11 +32,9 @@ def layout(value: str = "1") -> html:
# get all necessary data of the selected company
selected_company_stats = data_elements.get_company_data(session).loc[company_id]
selected_company_name = selected_company_stats.loc["company_name"]
finance_df = data_elements.get_finance_data(session)
selected_finance_df = finance_df.loc[finance_df["company_id"] == value]
# create all company page divs
return (
header_elements.create_selection_header(selected_company_name),
company_elements.create_company_stats(selected_company_stats),
company_elements.create_tabs(company_id, selected_finance_df),
company_elements.create_tabs(session, company_id),
)

View File

@ -21,18 +21,4 @@ def test_create_company_stats(full_db: Session) -> None:
def test_create_tabs(full_db: Session) -> None:
"""Checks if the tabs of the company page can be created."""
selected_company_id = 1
finance_df = data_elements.get_finance_data(full_db)
selected_finance_df = finance_df.loc[
finance_df["company_id"] == selected_company_id
]
company_elements.create_tabs(selected_company_id, selected_finance_df)
def test_kennzahlen_layout(full_db: Session) -> None:
"""Checks if the financial metric layout of the company page can be created."""
selected_company_id = 1
finance_df = data_elements.get_finance_data(full_db)
selected_finance_df = finance_df.loc[
finance_df["company_id"] == selected_company_id
]
company_elements.kennzahlen_layout(selected_finance_df)
company_elements.create_tabs(full_db, selected_company_id)

View File

@ -0,0 +1,39 @@
"""Tests for finance ui elements."""
import pandas as pd
from sqlalchemy.orm import Session
from aki_prj23_transparenzregister.models.company import FinancialKPIEnum
from aki_prj23_transparenzregister.ui import data_elements, finance_elements
def test_financial_metrics_layout(full_db: Session) -> None:
"""Checks if the financial metric layout of the company page can be created."""
selected_company_id = 1
finance_elements.financial_metrics_layout(full_db, selected_company_id)
def test_complete_metrics() -> None:
"""Tests if names for all the FinancialKPIEnum are there."""
assert set(finance_elements.METRICS.keys()).issubset(
{_.value for _ in FinancialKPIEnum}
)
def test_update_figure(full_db: Session) -> None:
"""Checks if the output after choosing a metric can be updated."""
company_id = 1
value = "equity"
data = data_elements.get_finance_data_of_one_company(full_db, company_id).to_json(
date_format="iso"
)
finance_elements.update_figure(value, data)
def test_finance_figure(full_db: Session) -> None:
"""Checks if the financial graph can be created."""
company_id = 1
metric = "equity"
data = data_elements.get_finance_data_of_one_company(full_db, company_id)
selected_finance_df = pd.DataFrame(data)
finance_elements.financials_figure(selected_finance_df, metric)