mirror of
https://github.com/fhswf/aki_prj23_transparenzregister.git
synced 2025-04-22 16:12:55 +02:00
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:
parent
c8d3c7395b
commit
9f7d714403
688
poetry.lock
generated
688
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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)
|
|
||||||
|
39
tests/ui/finance_elements_test.py
Normal file
39
tests/ui/finance_elements_test.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user