feat(ui): Include graph on person page and fix neighbor calculation (#533)

Includes the company/person relation graph on the persondetails page
incl. the number of relations on lvl 1,2 and 3, the calculation of which
was also fixed as part of the development.

The graph displayed on the company details page now also includes
**all** relations (company + person) until the 3rd lvl.

---------

Co-authored-by: Philipp Horstenkamp <philipp@horstenkamp.de>
This commit is contained in:
Tristan Nolde 2024-01-07 13:53:32 +01:00 committed by GitHub
parent 04bcc3c458
commit d55eeef87e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 217 additions and 48 deletions

View File

@ -15,10 +15,11 @@ from aki_prj23_transparenzregister.ui import (
sentiment_elements,
)
from aki_prj23_transparenzregister.utils.networkx.network_2d import create_2d_graph
from aki_prj23_transparenzregister.utils.networkx.network_base import initialize_network
from aki_prj23_transparenzregister.utils.networkx.network_base import (
initialize_network_without_metrics,
)
from aki_prj23_transparenzregister.utils.networkx.networkx_data import (
create_edge_and_node_list_for_company,
find_company_relations,
get_relations_until_level_3,
)
COLORS = {
@ -414,20 +415,16 @@ def network_layout(selected_company_id: int) -> html.Div:
Returns:
The html div to create the network tab of the company page.
"""
person_relations, company_relations = find_company_relations(selected_company_id)
# Create Edge and Node List from data
nodes, edges = create_edge_and_node_list_for_company(company_relations)
nodes, edges = get_relations_until_level_3(f"c_{str(selected_company_id)}")
# Initialize the Network and receive the Graph and a DataFrame with Metrics
if nodes != {}:
graph, metrics = initialize_network(nodes=nodes, edges=edges)
metric = "None"
graph = initialize_network_without_metrics(nodes=nodes, edges=edges)
figure = create_2d_graph(
graph,
nodes,
edges,
metrics,
metric,
pd.DataFrame(),
metric="None",
layout="Spring",
edge_annotation=True,
edge_thickness=1,

View File

@ -31,12 +31,7 @@ def layout(value: str = "1") -> html.Div:
# get all necessary data of the selected person
selected_person_stats = data_elements.get_person_data(session).loc[person_id]
selected_person_name = f"{selected_person_stats['person_firstname']} {selected_person_stats['person_lastname']}"
return (
person_elements.create_person_widgets(person_id, selected_person_name),
html.Div(
className="person-network", children=["Hier kann der Social Graph hin"]
),
)
return person_elements.create_person_widgets(person_id, selected_person_name)
@callback(

View File

@ -1,9 +1,15 @@
"""Dash elements for person page."""
import networkx as nx
from dash import html
import pandas as pd
from dash import dcc, html
from aki_prj23_transparenzregister.utils.networkx.network_2d import create_2d_graph
from aki_prj23_transparenzregister.utils.networkx.network_base import (
initialize_network_without_metrics,
)
from aki_prj23_transparenzregister.utils.networkx.networkx_data import (
get_relations_number_from_id,
get_relations_until_level_3,
)
@ -44,7 +50,7 @@ def create_person_widgets(selected_person_id: int, selected_person_name: str) ->
"""
try:
first_level, second_level, third_level = get_relations_number_from_id(
selected_person_id
f"p_{selected_person_id}"
)
except nx.exception.NetworkXError:
@ -67,13 +73,56 @@ def create_person_widgets(selected_person_id: int, selected_person_name: str) ->
],
),
create_small_widget(
["Anzahl Verbindungen", html.Br(), " - erste Ebene"], [first_level]
["Anzahl Verbindungen", html.Br(), "1. Ebene"], [first_level]
),
create_small_widget(
["Anzahl Verbindungen", html.Br(), " - dritte Ebene"], [second_level]
["Anzahl Verbindungen", html.Br(), "2. Ebene"], [second_level]
),
create_small_widget(
["Anzahl Verbindungen", html.Br(), " - dritte Ebene"], [third_level]
["Anzahl Verbindungen", html.Br(), "3. Ebene"], [third_level]
),
network_layout(selected_person_id),
],
)
def network_layout(selected_person_id: int) -> html.Div:
"""Create network tab.
Args:
selected_person_id: Id of the chosen person in the dropdown.
Returns:
The html div to create the network tab of the person page.
"""
nodes, edges = get_relations_until_level_3(f"p_{selected_person_id}")
# Initialize the Network and receive the Graph and a DataFrame with Metrics
if nodes != {}:
graph = initialize_network_without_metrics(nodes=nodes, edges=edges)
figure = create_2d_graph(
graph,
nodes,
edges,
pd.DataFrame([]),
metric="None",
layout="Spring",
edge_annotation=True,
edge_thickness=1,
)
return html.Div(
children=[
dcc.Graph(figure=figure, id="person-graph", className="graph-style")
]
)
return html.Div(
className="choose-metric",
children=[
html.H3(
className="metrics-title",
children=[
"Für diese Person wurden leider keine Verflechtungen gefunden."
],
)
],
)

View File

@ -72,6 +72,8 @@ def create_2d_graph( # noqa PLR0913
node_x.append(x)
node_y.append(y)
edge_type_list = [row["type"] for row in edges]
# Add the Edges to the scatter plot according to their Positions.
edge_trace = go.Scatter(
x=edge_x,
@ -80,16 +82,29 @@ def create_2d_graph( # noqa PLR0913
hoverinfo="none",
mode="lines",
)
# Add the Edge description text to the scatter plot according to its Position.
edge_weights_trace = go.Scatter(
x=edge_weight_x,
y=edge_weight_y,
mode="text",
marker_size=0.5,
text=[0.45, 0.7, 0.34],
textposition="top center",
hovertemplate="Relation: %{text}<extra></extra>",
)
if edge_annotation is True:
edge_weights_trace = go.Scatter(
x=edge_weight_x,
y=edge_weight_y,
mode="text",
marker_size=0.5,
text=[0.45, 0.7, 0.34],
textposition="top center",
hoverinfo="none",
)
else:
edge_weights_trace = go.Scatter(
x=edge_weight_x,
y=edge_weight_y,
mode="text",
marker_size=0.5,
text=[0.45, 0.7, 0.34],
textposition="top center",
hovertemplate="Relation: %{hovertext}<extra></extra>",
hovertext=edge_type_list,
)
# Add the Nodes to the scatter plot according to their Positions.
node_trace = go.Scatter(
@ -106,9 +121,7 @@ def create_2d_graph( # noqa PLR0913
# # Set Color by using the nodes DataFrame with its Color Attribute. The sequence matters!
colors = list(nx.get_node_attributes(graph, "color").values())
node_names = list(nx.get_node_attributes(graph, "name").values())
# ids = list(nx.get_node_attributes(graph, "id").values())
# # Get the Node Text
node_trace.marker.color = colors
node_trace.marker.line = {"color": "#2e2c2f", "width": 0.5}
node_trace.text = node_names
@ -118,10 +131,7 @@ def create_2d_graph( # noqa PLR0913
node_trace.marker.size = list(np.sqrt(metrics[metric]) * 200)
# Add Relation_Type as a Description for the edges.
if edge_annotation:
edge_type_list = [
row["type"] for row in edges
] # this code be moved and used as hover data
if edge_annotation is True: # this code be moved and used as hover data
edge_weights_trace.text = edge_type_list
# Return the Plotly Figure

View File

@ -283,6 +283,52 @@ def find_company_relations(
return pd.DataFrame(), company_relations # company_relations
def find_person_relations(
selected_person_id: int,
) -> pd.DataFrame:
"""Finds all Relations for the given Person id.
Args:
selected_person_id: Id of the Person which Relations should be returned.
Returns:
DataFrame
"""
session = SessionHandler.session
assert session # noqa: S101
relations_person_data = (
session.query(
entities.Company.id.label("id_company"),
entities.Company.name.label("name_company"),
entities.PersonRelation.relation.label("relation_type"),
entities.Person.id.label("id_person"),
entities.Person.lastname.label("lastname"),
entities.Person.firstname.label("firstname"),
entities.Person.date_of_birth.label("date_of_birth"),
)
.join(
entities.PersonRelation,
entities.PersonRelation.company_id == entities.Company.id,
)
.join(
entities.Person,
entities.PersonRelation.person_id == entities.Person.id,
)
.filter(entities.Person.id == selected_person_id)
.all()
)
person_relations = pd.DataFrame(relations_person_data) # type: ignore
person_relations["id_company"] = person_relations["id_company"].apply(
lambda x: f"c_{x}"
)
person_relations["id_person"] = person_relations["id_person"].apply(
lambda x: f"p_{x}"
)
return person_relations
def create_edge_and_node_list_for_company(
company_relations: pd.DataFrame,
) -> tuple[dict, list]:
@ -347,6 +393,18 @@ def get_all_metrics_from_id(company_id: int) -> pd.Series:
return filtered_metrics.iloc[0]
def filter_sets(sets: list[set]) -> set:
"""Filters the list of sets given to only contain unique entries.
Args:
sets (list[set]): List of sets to be filtered.
Returns:
list[set]: List of sets with only unique entries.
"""
return set.union(*sets)
@lru_cache
def get_relations_number_from_id(id: str) -> tuple[int, int, int]:
"""Returns all Relation in 1, 2 and 3 lvl of one Node.
@ -365,20 +423,80 @@ def get_relations_number_from_id(id: str) -> tuple[int, int, int]:
nodes_tmp, edges_tmp = create_edge_and_node_list(person_df, company_df)
graph = initialize_network_without_metrics(nodes=nodes_tmp, edges=edges_tmp)
neighbors = nx.all_neighbors(graph, id)
try:
neighbors = nx.all_neighbors(graph, id)
relations_lv1 = set(neighbors)
relations_lv2 = set()
relations_lv3 = set()
relations_lv1 = set(neighbors)
relations_lv2 = set()
relations_lv3 = set()
for node in relations_lv1:
relations_lv2 |= set(nx.all_neighbors(graph, node))
for node in relations_lv1:
relations_lv2 |= set(nx.all_neighbors(graph, node))
relations_lv2.discard(id)
relations_lv2.discard(id)
for sub_node in relations_lv2:
relations_lv3 |= set(nx.all_neighbors(graph, sub_node))
for sub_node in relations_lv2:
relations_lv3 |= set(nx.all_neighbors(graph, sub_node))
relations_lv2.difference(relations_lv3)
unique_entires = filter_sets([relations_lv1, relations_lv2])
relations_lv3 = {key for key in relations_lv3 if key not in unique_entires}
return len(relations_lv1), len(relations_lv2), len(relations_lv3)
return len(relations_lv1), len(relations_lv2), len(relations_lv3)
except nx.exception.NetworkXError:
return 0, 0, 0
@lru_cache
def get_relations_until_level_3(id: str) -> tuple[dict, list]:
"""Returns all Relation in 1, 2 and 3 lvl of one Node.
Args:
id: String of the Company or Person Id.
Returns:
tuple[dict, list]: nodes, edges
"""
# Get Data
person_df = get_all_person_relations()
company_df = get_all_company_relations()
# Create Edge and Node List from data
nodes_tmp, edges_tmp = create_edge_and_node_list(person_df, company_df)
graph = initialize_network_without_metrics(nodes=nodes_tmp, edges=edges_tmp)
try:
neighbors = nx.all_neighbors(graph, id)
relations_lv1 = set(neighbors)
relations_lv2 = set()
relations_lv3 = set()
for node in relations_lv1:
relations_lv2 |= set(nx.all_neighbors(graph, node))
relations_lv2.discard(id)
for sub_node in relations_lv2:
relations_lv3 |= set(nx.all_neighbors(graph, sub_node))
unique_entires = filter_sets([relations_lv1, relations_lv2])
relations_lv3 = {key for key in relations_lv3 if key not in unique_entires}
nodes = {
key: value
for key, value in nodes_tmp.items()
if (
key in relations_lv1
or key in relations_lv2
or key in relations_lv3
or key == id
)
}
edges = [
edge
for edge in edges_tmp
if (edge["from"] in nodes and edge["to"] in nodes)
]
return nodes, edges
except nx.exception.NetworkXError:
return {}, []