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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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( app = Dash(
__name__, __name__,
use_pages=True, use_pages=True,
suppress_callback_exceptions=True,
external_stylesheets=[dbc.icons.BOOTSTRAP], external_stylesheets=[dbc.icons.BOOTSTRAP],
title="Transparenzregister", title="Transparenzregister",
) # use dbc for icons ) # use dbc for icons

View File

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

View File

@ -1,6 +1,6 @@
.tabs { .tabs {
float: left; float: left;
margin-top: 20px; margin-top: 10px;
border: 1px solid; border: 1px solid;
width: 100%; width: 100%;
} }
@ -21,3 +21,31 @@
background-color: var(--paynes-gray) !important; background-color: var(--paynes-gray) !important;
font-weight: bold !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.""" """Dash elements."""
import pandas as pd import pandas as pd
from dash import dash_table, dcc, html from dash import dash_table, dcc, html
from sqlalchemy.orm import Session
from aki_prj23_transparenzregister.ui import finance_elements 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. """Create tabs for more company information.
Args: Args:
session: A session connecting to the database.
selected_company_id: Id of the chosen company in the dropdown. 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: Returns:
The html div to create the tabs of the company page. 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", value="tab-1",
className="tab-style", className="tab-style",
selected_className="selected-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( dcc.Tab(
label="Beteiligte Personen", 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: def network_layout(selected_company_id: int) -> html:
"""Create network tab. """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) 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 @cached( # type: ignore
cache=TTLCache(maxsize=1, ttl=300), cache=TTLCache(maxsize=1, ttl=300),
key=lambda session: 0 if session is None else str(session.bind), 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 = get_person_data(session).rename("p_{}".format)
persons["person_lastname_firstname"] = ( persons["person_lastname_firstname"] = (
persons["person_surname"] + ", " + persons["person_name"] persons["person_lastname"] + ", " + persons["person_firstname"]
) )
persons_options = persons["person_lastname_firstname"] persons_options = persons["person_lastname_firstname"]
companies = get_company_data(session).rename("c_{}".format) companies = get_company_data(session).rename("c_{}".format)

View File

@ -1,7 +1,13 @@
"""Finance elements for Dash.""" """Finance elements for Dash."""
from io import StringIO
import pandas as pd import pandas as pd
import plotly.graph_objs as go 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 = { COLORS = {
"light": "#edefef", "light": "#edefef",
@ -12,6 +18,126 @@ COLORS = {
"raisin-black": "#2e2c2f", "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: def financials_figure(selected_finance_df: pd.DataFrame, metric: str) -> go.Figure:
"""Creates plotly line chart for a specific company and a metric. """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 # add trace for company 1
fig_line.add_trace( fig_line.add_trace(
go.Scatter( go.Scatter(
x=selected_finance_df["annual_finance_statement_date"], x=selected_finance_df["date"],
y=selected_finance_df[metric], y=selected_finance_df[metric],
line_color=COLORS["raisin-black"], line_color=COLORS["raisin-black"],
marker_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 # set title and labels
fig_line.update_layout( fig_line.update_layout(
title=metric, title="Entwicklungsverlauf: " + METRICS[metric],
xaxis_title="Jahr", yaxis_title=f"{METRICS[metric]} in Euro (€)",
yaxis_title="in Mio.€",
plot_bgcolor=COLORS["light"], plot_bgcolor=COLORS["light"],
) )
return fig_line return fig_line

View File

@ -32,11 +32,9 @@ def layout(value: str = "1") -> html:
# get all necessary data of the selected company # get all necessary data of the selected company
selected_company_stats = data_elements.get_company_data(session).loc[company_id] selected_company_stats = data_elements.get_company_data(session).loc[company_id]
selected_company_name = selected_company_stats.loc["company_name"] 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 # create all company page divs
return ( return (
header_elements.create_selection_header(selected_company_name), header_elements.create_selection_header(selected_company_name),
company_elements.create_company_stats(selected_company_stats), 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: def test_create_tabs(full_db: Session) -> None:
"""Checks if the tabs of the company page can be created.""" """Checks if the tabs of the company page can be created."""
selected_company_id = 1 selected_company_id = 1
finance_df = data_elements.get_finance_data(full_db) company_elements.create_tabs(full_db, selected_company_id)
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)

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)