Added multi relation dropdowns to dashbord (#363)

This change allows for a more complete combination of relation
combinations to be filtered.
This commit is contained in:
Philipp Horstenkamp 2023-11-11 13:47:46 +01:00 committed by GitHub
parent ad8f5d0fb1
commit e5b61bc19c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 106 deletions

View File

@ -154,7 +154,7 @@ jobs:
run: exit 0
- name: Exit workflow on not main branch
if: ${{ github.ref != 'refs/heads/main'}}
if: ${{ github.ref != 'refs/heads/main' }}
run: exit 0
- name: Login to GitHub Container Registry

View File

@ -10,7 +10,7 @@
height: 100%;
}
.top_companytable_style {
.top_company_table_style {
float: left;
margin-top: 20px;
margin-left: 20px;
@ -91,7 +91,7 @@
}
.networkx_style .graph-style {
margin-top: 10px;
padding-bottom: 0px;
display: inline-block;

View File

@ -4,7 +4,6 @@ from functools import lru_cache
import dash
import dash_daq as daq
import networkx as nx
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from cachetools import TTLCache, cached
@ -40,50 +39,113 @@ dash.register_page(
# ],
)
metric = "None"
switch_edge_annotaion_value = False
egde_thickness = 1
network = None
def person_relation_type_filter() -> list[str]:
"""Returns a Numpy Array of String with Person relation types."""
return get_all_person_relations()["relation_type"].unique().tolist()
def person_relation_type_filter() -> np.ndarray:
"""Returns an Numpy Array of String with Person telation types."""
return get_all_person_relations()["relation_type"].unique()
def company_relation_type_filter() -> np.ndarray:
"""Returns an Numpy Array of String with Company relation types."""
return get_all_company_relations()["relation_type"].unique()
def company_relation_type_filter() -> list[str]:
"""Returns a Numpy Array of String with Company relation types."""
return get_all_company_relations()["relation_type"].unique().tolist()
def update_table(
metric_dropdown_value: str, metrics: pd.DataFrame
) -> tuple[dict, list]:
"""_summary_.
Args:
metric_dropdown_value (str): _description_
metrics (pd.DataFrame): _description_
Returns:
tuple[dict, list]: _description_
"""
"""_summary_."""
table_df = metrics.sort_values(metric_dropdown_value, ascending=False).head(10)
table_df = table_df[["designation", "category", metric_dropdown_value]]
columns = [{"name": i, "id": i} for i in table_df.columns]
return table_df.to_dict("records"), columns # type: ignore
def layout() -> list[html.Div]:
@cached(TTLCache(20, ttl=600))
def _update_figure( # noqa: PLR0913
selected_metric: str,
switch_value: bool,
switch_edge_annotation_value: bool,
c_relation_filter_value: frozenset[str],
p_relation_filter_value: frozenset[str],
layout: str,
slider_value: float,
metric_dropdown_value: str,
) -> tuple[dict, list, go.Figure]:
"""In this Callback the Value of the Dropdown is used to filter the Data.
In Addition, it takes the filter for the Graph metrics and creates a new graph, or switches between 3D and 2D.
Args:
selected_metric: Selected Value
switch_value: True if 2D, False if 3D
switch_edge_annotation_value: True if Edge should have a description, False = No Description
c_relation_filter_value: Variable with String value of Relation Type for Companies
p_relation_filter_value: Variable with String value of Relation Type for Persons
layout: String of the Layout Dropdown
metric_dropdown_value: String of the Metric Dropdown
slider_value: Sets the size of the Edge Connections
Returns:
Network Graph(Plotly Figure): Plotly Figure in 3 or 2D
"""
_ = c_relation_filter_value, p_relation_filter_value
graph, metrics, nodes, edges = update_graph_data(
person_relation_type=p_relation_filter_value,
company_relation_type=c_relation_filter_value,
)
table_dict, table_columns = update_table(metric_dropdown_value, metrics)
if switch_value:
return (
table_dict,
table_columns,
create_2d_graph(
graph,
nodes,
edges,
metrics,
selected_metric,
layout,
switch_edge_annotation_value,
slider_value, # type: ignore
),
)
return (
table_dict,
table_columns,
create_3d_graph(
graph,
nodes,
edges,
metrics,
selected_metric,
layout,
switch_edge_annotation_value,
slider_value, # type: ignore
),
)
def layout() -> list[html]:
"""Generates the Layout of the Homepage."""
person_relation_types = person_relation_type_filter()
company_relation_types = company_relation_type_filter()
top_companies_dict, top_companies_columns, figure = update_figure(
selected_company_relation_types: frozenset[str] = frozenset(
{company_relation_types[1]}
)
selected_person_relation_types: frozenset[str] = frozenset(
{}
) # frozenset({person_relation_types[1]})
top_companies_dict, top_companies_columns, figure = _update_figure(
"None",
False,
False,
company_relation_types[0],
person_relation_types[0],
selected_company_relation_types,
selected_person_relation_types,
"Spring",
1,
"degree",
@ -92,7 +154,7 @@ def layout() -> list[html.Div]:
children=html.Div(
children=[
html.Div(
className="top_companytable_style",
className="top_company_table_style",
children=[
html.H1(
title="Top Ten Nodes in Graph by Metric",
@ -143,9 +205,11 @@ def layout() -> list[html.Div]:
),
dcc.Dropdown(
company_relation_types,
company_relation_types[0],
list(selected_company_relation_types),
id="dropdown_company_relation_filter",
className="dropdown_style",
multi=True,
persistence=True,
),
],
),
@ -159,9 +223,11 @@ def layout() -> list[html.Div]:
),
dcc.Dropdown(
person_relation_types,
person_relation_types[0],
list(selected_person_relation_types),
id="dropdown_person_relation_filter",
className="dropdown_style",
multi=True,
persistence=True,
),
],
),
@ -282,7 +348,10 @@ def layout() -> list[html.Div]:
],
),
dcc.Graph(
figure=figure, id="my-graph", className="graph-style"
figure=figure,
id="my-graph",
className="graph-style",
config={"displaylogo": False},
),
],
),
@ -293,8 +362,8 @@ def layout() -> list[html.Div]:
@lru_cache(200)
def update_graph_data(
person_relation_type: str = "HAFTENDER_GESELLSCHAFTER",
company_relation_type: str = "GESCHAEFTSFUEHRER",
person_relation_type: frozenset[str] | None = None,
company_relation_type: frozenset[str] | None = None,
) -> tuple[nx.Graph, pd.DataFrame, dict, list]:
"""_summary_.
@ -305,7 +374,6 @@ def update_graph_data(
Returns:
tuple[nx.Graph, pd.DataFrame, dict, list]: _description_
"""
# Get Data
person_df = get_all_person_relations()
company_df = get_all_company_relations()
@ -339,71 +407,41 @@ def update_graph_data(
prevent_initial_call=True,
allow_duplicate=True,
)
# @lru_cache(20)
@cached(cache=TTLCache(maxsize=100, ttl=500))
def update_figure( # noqa: PLR0913
selected_metric: str,
switch_value: bool,
# switch_node_annotaion_value: bool,
switch_edge_annotaion_value: bool,
c_relation_filter_value: str,
p_relation_filter_value: str,
switch_edge_annotation_value: bool,
c_relation_filter_value: list[str],
p_relation_filter_value: list[str],
layout: str,
slider_value: float,
metric_dropdown_value: str,
) -> go.Figure:
"""In this Callback the Value of the Dropdown is used to filter the Data. In Addition it takes the filter for the Graph metrics and creates a new graph, or switches between 3D and 2D.
) -> tuple[dict, list, go.Figure]:
"""In this Callback the Value of the Dropdown is used to filter the Data.
In Addition, it takes the filter for the Graph metrics and creates a new graph, or switches between 3D and 2D.
Args:
selected_metric (_type_): Selected Value
switch_value (bool): True if 2D, False if 3D
switch_edge_annotaion_value: True if Edge should have a description, Flase = No Descritpion
c_relation_filter_value (_type_): Variable with String value of Relation Type for Companies
p_relation_filter_value (_type_): Variable with String value of Relation Type for Persons
selected_metric: Selected Value
switch_value: True if 2D, False if 3D
switch_edge_annotation_value: True if Edge should have a description, False = No Description
c_relation_filter_value: Variable with String value of Relation Type for Companies
p_relation_filter_value: Variable with String value of Relation Type for Persons
layout: String of the Layout Dropdown
metric_dropdown_value: String of the Metric Dropdown
slider_value: Sets the size of the Edge Connections
Returns:
Network Graph(Plotly Figure): Plotly Figure in 3 or 2D
Network Graph: Plotly Figure in 3D or 2D
"""
_ = c_relation_filter_value, p_relation_filter_value
graph, metrics, nodes, edges = update_graph_data(
person_relation_type=p_relation_filter_value,
company_relation_type=c_relation_filter_value,
)
table_dict, table_columns = update_table(metric_dropdown_value, metrics)
if switch_value:
return (
table_dict,
table_columns,
create_2d_graph(
graph,
nodes,
edges,
metrics,
selected_metric,
layout,
switch_edge_annotaion_value,
slider_value, # type: ignore
),
)
return (
table_dict,
table_columns,
create_3d_graph(
graph,
nodes,
edges,
metrics,
selected_metric,
layout,
switch_edge_annotaion_value,
slider_value, # type: ignore
),
return _update_figure(
selected_metric,
switch_value,
switch_edge_annotation_value,
frozenset(c_relation_filter_value),
frozenset(p_relation_filter_value),
layout,
slider_value,
metric_dropdown_value,
)

View File

@ -18,11 +18,11 @@ def create_3d_graph( # noqa : PLR0913
"""This Method creates a 3D Network in Plotly with a Scatter Graph and retuns it.
Args:
graph (_type_): NetworkX Graph.
nodes (_type_): List of Nodes
edges (_type_): List of Edges
metrics (_type_): DataFrame with the MEtrics
metric (_type_): Selected Metric
graph: NetworkX Graph.
nodes: List of Nodes
edges: List of Edges
metrics: DataFrame with the Metrics
metric: Selected Metric
Returns:
_type_: Plotly Figure
@ -180,7 +180,7 @@ def create_3d_graph( # noqa : PLR0913
node_trace.marker.color = colors
node_trace.text = node_names
# Highlight the Node Size in regards to the selected Metric.
# Highlight the Node Size with regard to the selected Metric.
if metric != "None":
node_trace.marker.size = list(np.cbrt(metrics[metric]) * 200)

View File

@ -23,7 +23,6 @@ def initialize_network(edges: list, nodes: dict) -> tuple[nx.Graph, pd.DataFrame
# update node attributes from dataframe
nx.set_node_attributes(graph, nodes)
# Create a DataFrame with all Metrics
# Create a DataFrame with all Metrics
metrics = pd.DataFrame(
columns=[
"eigenvector",
@ -36,7 +35,7 @@ def initialize_network(edges: list, nodes: dict) -> tuple[nx.Graph, pd.DataFrame
"id",
]
)
metrics["eigenvector"] = nx.eigenvector_centrality(graph).values()
# metrics["eigenvector"] = nx.eigenvector_centrality(graph, 200).values()
metrics["degree"] = nx.degree_centrality(graph).values()
metrics["betweenness"] = nx.betweenness_centrality(graph).values()
metrics["closeness"] = nx.closeness_centrality(graph).values()
@ -54,8 +53,8 @@ def initialize_network_with_reduced_metrics(
"""This Method creates a Network from the Framework NetworkX with the help of a Node and Edge List. Furthemore it creates a DataFrame with the most important Metrics.
Args:
edges (list): List with the connections between Nodes.
nodes (dict): Dict with all Nodes.
edges: List with the connections between Nodes.
nodes: Dict with all Nodes.
Returns:
Graph: Plotly Figure

View File

@ -3,6 +3,7 @@ from functools import lru_cache
import networkx as nx
import pandas as pd
from loguru import logger
from sqlalchemy.orm import aliased
from aki_prj23_transparenzregister.ui.session_handler import SessionHandler
@ -103,7 +104,7 @@ def get_all_company_relations() -> pd.DataFrame:
def get_all_person_relations() -> pd.DataFrame:
"""This Methods makes a Database Request for all Persons and their relations, modifies the ID Column and returns the Result as an DataFrame.
"""These method makes a Database Request for all Persons and their relations, modifies the ID Column and returns the Result as an DataFrame.
Returns:
DataFrame: DataFrame with all Relations between Persons and Companies.
@ -142,7 +143,7 @@ def get_all_person_relations() -> pd.DataFrame:
def filter_relation_type(
relation_dataframe: pd.DataFrame, selected_relation_type: str
relation_dataframe: pd.DataFrame, selected_relation_type: frozenset[str] | None
) -> pd.DataFrame:
"""This Method filters the given DataFrame based on the selected Relation Type and returns it.
@ -153,9 +154,13 @@ def filter_relation_type(
Returns:
relation_dataframe (pd.DataFrame): The filtered DataFrame which now only contains entries with the selected Relation Type.
"""
return relation_dataframe.loc[
relation_dataframe["relation_type"] == selected_relation_type
]
if selected_relation_type is not None:
filtered = relation_dataframe.loc[
relation_dataframe["relation_type"].isin(selected_relation_type)
]
logger.info(f"Filtered! {len(filtered.index) / len(relation_dataframe)}")
return filtered
return relation_dataframe
def create_edge_and_node_list(

View File

@ -187,9 +187,8 @@ def _get_company_relations() -> Generator:
yield
@pytest.mark.tim()
def test_update_graph_data() -> None:
graph_result, metrics_result, nodes_result, edges_result = home.update_graph_data(
"HAFTENDER_GESELLSCHAFTER", "GESCHAEFTSFUEHRER"
frozenset({"HAFTENDER_GESELLSCHAFTER"}), frozenset("GESCHAEFTSFUEHRER")
)
assert graph_result is not None

View File

@ -185,7 +185,7 @@ def test_filter_relation_type() -> None:
relation_dataframe = networkx_data.get_all_company_relations()
selected_relation_type = "HAFTENDER_GESELLSCHAFTER"
company_relations_df = networkx_data.filter_relation_type(
relation_dataframe, selected_relation_type
relation_dataframe, frozenset({selected_relation_type})
)
assert type(company_relations_df) is pd.DataFrame