dynamic properties via JS in dash-cytoscape

This commit is contained in:
Florian Förster 2024-06-12 16:06:30 +02:00
parent b3e35e7dd1
commit c2714b8060
6 changed files with 328 additions and 114 deletions

56
lang_main_config.toml Normal file
View File

@ -0,0 +1,56 @@
# lang_main: Config file
[paths]
inputs = './inputs/'
results = './results/test_new2/'
dataset = './01_2_Rohdaten_neu/Export4.csv'
#results = './results/Export7/'
#dataset = './01_03_Rohdaten_202403/Export7_59499_Zeilen.csv'
#results = './results/Export7_trunc/'
#dataset = './01_03_Rohdaten_202403/Export7_trunc.csv'
[control]
preprocessing = true
preprocessing_skip = false
token_analysis = false
token_analysis_skip = false
graph_postprocessing = false
graph_postprocessing_skip = false
time_analysis = false
time_analysis_skip = false
#[export_filenames]
#filename_cossim_filter_candidates = 'CosSim-FilterCandidates'
[preprocess]
filename_cossim_filter_candidates = 'CosSim-FilterCandidates'
date_cols = [
"VorgangsDatum",
"ErledigungsDatum",
"Arbeitsbeginn",
"ErstellungsDatum",
]
threshold_amount_characters = 5
threshold_similarity = 0.8
[graph_postprocessing]
threshold_edge_weight = 150
[time_analysis.uniqueness]
threshold_unique_texts = 4
criterion_feature = 'HObjektText'
feature_name_obj_id = 'ObjektID'
[time_analysis.model_input]
input_features = [
'VorgangsTypName',
'VorgangsArtText',
'VorgangsBeschreibung',
]
activity_feature = 'VorgangsTypName'
activity_types = [
'Reparaturauftrag (Portal)',
'Störungsmeldung',
]
threshold_num_acitivities = 1
threshold_similarity = 0.8

View File

@ -1,6 +1,6 @@
[project] [project]
name = "lang-main" name = "lang-main"
version = "0.1.0" version = "0.1.0dev1"
description = "Several tools to analyse maintenance data with strong focus on language processing" description = "Several tools to analyse maintenance data with strong focus on language processing"
authors = [ authors = [
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"}, {name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
@ -40,6 +40,7 @@ trials = [
line-length = 94 line-length = 94
indent-width = 4 indent-width = 4
target-version = "py311" target-version = "py311"
src = ["src"]
[tool.ruff.format] [tool.ruff.format]
quote-style = "single" quote-style = "single"

View File

@ -3,26 +3,74 @@ import webbrowser
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
from typing import cast from typing import cast
import copy
import dash_cytoscape as cyto import dash_cytoscape as cyto
import lang_main.io
from dash import Dash, Input, Output, State, dcc, html from dash import Dash, Input, Output, State, dcc, html
from dash.exceptions import PreventUpdate
import lang_main.io
from lang_main.analysis import graphs from lang_main.analysis import graphs
target = '../results/test_20240529/Pipe-Token_Analysis_Step-1_build_token_graph.pkl' target = '../results/test_20240529/Pipe-Token_Analysis_Step-1_build_token_graph.pkl'
p = Path(target).resolve() p = Path(target).resolve()
ret = lang_main.io.load_pickle(p) ret = lang_main.io.load_pickle(p)
tk_graph = cast(graphs.TokenGraph, ret[0]) tk_graph = cast(graphs.TokenGraph, ret[0])
tk_graph_filtered = tk_graph.filter_by_edge_weight(150) tk_graph_filtered = tk_graph.filter_by_edge_weight(150, None)
tk_graph_filtered = tk_graph_filtered.filter_by_node_degree(1) tk_graph_filtered = tk_graph_filtered.filter_by_node_degree(1, None)
cyto_data, weight_data = graphs.convert_graph_to_cytoscape(tk_graph_filtered) cyto_data_base, weight_data = graphs.convert_graph_to_cytoscape(tk_graph_filtered)
MIN_WEIGHT = weight_data['min'] MIN_WEIGHT = weight_data['min']
MAX_WEIGHT = weight_data['max'] MAX_WEIGHT = weight_data['max']
cyto.load_extra_layouts() cyto.load_extra_layouts()
app = Dash(__name__) external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = Dash(__name__, external_stylesheets=external_stylesheets)
cose_layout = {
'name': 'cose',
'nodeOverlap': 20,
'refresh': 20,
'fit': True,
'padding': 30,
'randomize': True,
'componentSpacing': 40,
'nodeRepulsion': 2000,
'edgeElasticity': 1000,
'idealEdgeLength': 100,
'nestingFactor': 1.2,
'gravity': 50,
'numIter': 2000,
'initialTemp': 1000,
'coolingFactor': 0.95,
'minTemp': 1.0,
'nodeDimensionsIncludeLabels': True,
}
cose_bilkent_layout = {
'name': 'cose-bilkent',
'nodeDimensionsIncludeLabels': True,
'idealEdgeLength': 100,
'edgeElasticity': 0.45,
'nodeRepulsion': 10000,
'nestingFactor': 0.1,
'gravity': 0.25,
'numIter': 2500,
'initialTemp': 1000,
'coolingFactor': 0.95,
'minTemp': 1.0,
}
cola_layout = {
'name': 'cola',
'nodeDimensionsIncludeLabels': True,
'nodeSpacing': 30,
'edgeLength': 45,
'animate': True,
'centerGraph': True,
'randomize': False,
}
my_stylesheet = [ my_stylesheet = [
# Group selectors # Group selectors
@ -45,7 +93,10 @@ my_stylesheet = [
{ {
'selector': 'edge', 'selector': 'edge',
'style': { 'style': {
'width': 2, #'width': f'mapData(weight, {MIN_WEIGHT}, {MAX_WEIGHT}, 1, 10)',
# 'width': """function(ele) {
# return ele.data('weight');
# """,
'curve-style': 'bezier', 'curve-style': 'bezier',
'line-color': 'grey', 'line-color': 'grey',
'line-style': 'solid', 'line-style': 'solid',
@ -59,130 +110,201 @@ my_stylesheet = [
app.layout = html.Div( app.layout = html.Div(
[ [
html.Button('Reset', id='bt-reset'), html.Button('Trigger JS Layout', id='test_js'),
html.Button('Trigger JS Weight', id='test_js_weight'),
html.Div(id='output'),
html.Div(
[
html.H2('Token Graph', style={'margin': 0}),
html.Button(
'Reset Default',
id='bt-reset',
style={
'marginLeft': 'auto',
'width': '300px',
},
),
],
style={
'display': 'flex',
'marginBottom': '1em',
},
),
html.H3('Layout'),
dcc.Dropdown( dcc.Dropdown(
id='layout_choice_internal', id='layout_choice',
options=[ options=[
'random',
'grid',
'circle',
'concentric',
'breadthfirst',
'cose', 'cose',
'cola',
'euler',
'random',
], ],
value='cose', value='cose',
clearable=False, clearable=False,
), ),
dcc.Dropdown( html.Div(
id='layout_choice_external', [
options=[ html.H3('Graph Filter'),
'cose-bilkent', dcc.Input(
'cola', id='weight_min',
'euler', type='number',
'spread',
'dagre',
'klay',
],
clearable=False,
),
dcc.RangeSlider(
id='weight_slider',
min=MIN_WEIGHT, min=MIN_WEIGHT,
max=MAX_WEIGHT, max=MAX_WEIGHT,
step=1000, step=1,
placeholder=f'Minimum edge weight: {MIN_WEIGHT} - {MAX_WEIGHT}',
debounce=True,
style={'width': '40%'},
), ),
dcc.Input(
id='weight_max',
type='number',
min=MIN_WEIGHT,
max=MAX_WEIGHT,
step=1,
placeholder=f'Maximum edge weight: {MIN_WEIGHT} - {MAX_WEIGHT}',
debounce=True,
style={'width': '40%'},
),
html.H3('Graph'),
html.Div(
[
cyto.Cytoscape( cyto.Cytoscape(
id='cytoscape-graph', id='cytoscape-graph',
layout={'name': 'cose'},
style={'width': '100%', 'height': '600px'}, style={'width': '100%', 'height': '600px'},
stylesheet=my_stylesheet, stylesheet=my_stylesheet,
elements=cyto_data, elements=cyto_data_base,
zoom=1, zoom=1,
), ),
] ],
style={
'border': '3px solid black',
'borderRadius': '25px',
'marginTop': '1em',
'marginBottom': '2em',
'padding': '7px',
},
),
],
style={'marginTop': '1em'},
),
],
style={'margin': '2em'},
) )
@app.callback( @app.callback(
Output('cytoscape-graph', 'layout', allow_duplicate=True), Output('cytoscape-graph', 'layout', allow_duplicate=True),
Input('layout_choice_internal', 'value'), Input('layout_choice', 'value'),
prevent_initial_call=True, prevent_initial_call=True,
) )
def update_layout_internal(layout_choice): def update_layout_internal(layout_choice):
return {'name': layout_choice} # return {'name': layout_choice}
return cose_layout
# return cose_bilkent_layout
@app.callback( # return cola_layout
Output('cytoscape-graph', 'layout', allow_duplicate=True),
Input('layout_choice_external', 'value'),
prevent_initial_call=True,
)
def update_layout_external(layout_choice):
return {'name': layout_choice}
@app.callback( @app.callback(
Output('cytoscape-graph', 'zoom'), Output('cytoscape-graph', 'zoom'),
Output('cytoscape-graph', 'elements'), Output('cytoscape-graph', 'elements', allow_duplicate=True),
Output('weight_min', 'value'),
Output('weight_max', 'value'),
Input('bt-reset', 'n_clicks'), Input('bt-reset', 'n_clicks'),
prevent_initial_call=True, prevent_initial_call=True,
) )
def reset_layout(n_clicks): def reset_layout(n_clicks):
return (1, cyto_data) return (1, cyto_data_base, None, None)
# @app.callback( # update edge weight
# Output('cytoscape-graph', 'stylesheet'), @app.callback(
# Input('weight_slider', 'value'), Output('cytoscape-graph', 'elements', allow_duplicate=True),
# State('cytoscape-graph', 'stylesheet'), Input('weight_min', 'value'),
Input('weight_max', 'value'),
prevent_initial_call=True,
)
def update_edge_weight(weight_min, weight_max):
if not any([weight_min, weight_max]):
return cyto_data_base
if weight_min is None:
weight_min = MIN_WEIGHT
if weight_max is None:
weight_max = MAX_WEIGHT
tk_graph_filtered = tk_graph.filter_by_edge_weight(weight_min, weight_max)
tk_graph_filtered = tk_graph_filtered.filter_by_node_degree(1, None)
cyto_data, _ = graphs.convert_graph_to_cytoscape(tk_graph_filtered)
return cyto_data
# app.clientside_callback(
# """
# function(n_clicks, layout) {
# let threshold = 1000;
# layout.edgeLength = function(edge) {
# let weight = edge.data().weight;
# let length;
# if (weight > threshold) {
# length = 10;
# } else {
# length = 1000 / edge.data().weight;
# length = Math.max(20, length);
# }
# return length;
# };
# cy.layout(layout).run();
# return layout;
# }
# """,
# Output('cytoscape-graph', 'layout', allow_duplicate=True),
# Input('test_js', 'n_clicks'),
# State('cytoscape-graph', 'layout'),
# prevent_initial_call=True, # prevent_initial_call=True,
# ) # )
# def select_weight(range_chosen, stylesheet):
# min_weight, max_weight = range_chosen
# new_stylesheet = stylesheet.copy()
# new_stylesheet.append(
# {
# 'selector': f'[weight >= {min_weight}]',
# 'style': {'line-color': 'blue', 'line-style': 'dashed'},
# }
# )
# new_stylesheet.append(
# {
# 'selector': f'[weight <= {max_weight}]',
# 'style': {'line-color': 'blue', 'line-style': 'dashed'},
# }
# )
# return new_stylesheet
app.clientside_callback(
"""
function(n_clicks, layout) {
layout.edgeElasticity = function(edge) {
return edge.data().weight * 4;
};
layout.idealEdgeLength = function(edge) {
return edge.data().weight * 0.8;
};
cy.layout(layout).run();
return layout;
}
""",
Output('cytoscape-graph', 'layout', allow_duplicate=True),
Input('test_js', 'n_clicks'),
State('cytoscape-graph', 'layout'),
prevent_initial_call=True,
)
# app.layout = html.Div( app.clientside_callback(
# [ """
# cyto.Cytoscape( function(n_clicks, stylesheet) {
# id='cytoscape-two-nodes', function edge_weight(ele) {
# layout={'name': 'preset'}, let threshold = 1000;
# style={'width': '100%', 'height': '400px'}, let weight = ele.data('weight');
# stylesheet=my_stylesheet, if (weight > threshold) {
# elements=[ weight = 12;
# { } else {
# 'data': { weight = weight / threshold * 10;
# 'id': 'one', weight = Math.max(1, weight);
# 'label': 'Titel 1', }
# }, return weight;
# 'position': {'x': 75, 'y': 75}, }
# 'grabbable': False, stylesheet[1].style.width = edge_weight;
# #'locked': True, cy.style(stylesheet).update();
# 'classes': 'red', return stylesheet;
# }, }
# { """,
# 'data': {'id': 'two', 'label': 'Title 2'}, Output('cytoscape-graph', 'stylesheet'),
# 'position': {'x': 200, 'y': 200}, Input('test_js_weight', 'n_clicks'),
# 'classes': 'triangle', State('cytoscape-graph', 'stylesheet'),
# }, prevent_initial_call=False,
# {'data': {'source': 'one', 'target': 'two', 'weight': 2000}}, )
# ],
# )
# ]
# )
def _start_webbrowser(): def _start_webbrowser():

18
scripts/dashboard/test.py Normal file
View File

@ -0,0 +1,18 @@
from pathlib import Path
from typing import cast
import statistics
import lang_main.io
from lang_main.analysis import graphs
# target = '../results/test_20240529/Pipe-Token_Analysis_Step-1_build_token_graph.pkl'
# p = Path(target).resolve()
# ret = lang_main.io.load_pickle(p)
# tk_graph = cast(graphs.TokenGraph, ret[0])
# tk_graph_filtered = tk_graph.filter_by_edge_weight(150, None)
# tk_graph_filtered = tk_graph_filtered.filter_by_node_degree(1, None)
# cyto_data_base, weight_data, all_weights = graphs.convert_graph_to_cytoscape(tk_graph_filtered)
test = [1, 1, 1, 2, 2, 3, 3, 4, 4, 1000]
print(statistics.mean(test))

View File

@ -130,7 +130,7 @@ def convert_graph_to_cytoscape(
cyto_data: list[CytoscapeData] = [] cyto_data: list[CytoscapeData] = []
# iterate over nodes # iterate over nodes
nodes = cast(Iterable[NodeTitle], graph.nodes) nodes = cast(Iterable[NodeTitle], graph.nodes)
for i, node in enumerate(nodes): for node in nodes:
node_data: CytoscapeData = { node_data: CytoscapeData = {
'data': { 'data': {
'id': node, 'id': node,
@ -151,7 +151,7 @@ def convert_graph_to_cytoscape(
], ],
graph.edges.data('weight', default=1), # type: ignore graph.edges.data('weight', default=1), # type: ignore
) )
for i, (source, target, weight) in enumerate(edges): for source, target, weight in edges:
weights.add(weight) weights.add(weight)
edge_data: CytoscapeData = { edge_data: CytoscapeData = {
'data': { 'data': {
@ -288,27 +288,35 @@ class TokenGraph(DiGraph):
def filter_by_edge_weight( def filter_by_edge_weight(
self, self,
threshold: int, bound_lower: int | None,
bound_upper: int | None,
) -> Self: ) -> Self:
"""filters all edges which are below the given threshold """filters all edges which are within the provided bounds
Parameters Parameters
---------- ----------
threshold : int bound_lower : int | None
edges with weights smaller than this value will be removed lower bound for edge weights, edges with weight equal to this value are retained
bound_upper : int | None
upper bound for edge weights, edges with weight equal to this value are retained
Returns Returns
------- -------
Self Self
a copy of the graph with filtered edges a copy of the graph with filtered edges
""" """
# filter edges by weight
original_graph_edges = copy.deepcopy(self.edges) original_graph_edges = copy.deepcopy(self.edges)
filtered_graph = self.copy() filtered_graph = self.copy()
if not any([bound_lower, bound_upper]):
logger.warning('No bounds provided, returning original graph.')
return filtered_graph
for edge in original_graph_edges: for edge in original_graph_edges:
weight = typing.cast(int, filtered_graph[edge[0]][edge[1]]['weight']) weight = typing.cast(int, filtered_graph[edge[0]][edge[1]]['weight'])
if weight < threshold: if bound_lower is not None and weight < bound_lower:
filtered_graph.remove_edge(edge[0], edge[1])
if bound_upper is not None and weight > bound_upper:
filtered_graph.remove_edge(edge[0], edge[1]) filtered_graph.remove_edge(edge[0], edge[1])
if filtered_graph._undirected is not None: if filtered_graph._undirected is not None:
@ -320,14 +328,17 @@ class TokenGraph(DiGraph):
def filter_by_node_degree( def filter_by_node_degree(
self, self,
threshold: int, bound_lower: int | None,
bound_upper: int | None,
) -> Self: ) -> Self:
"""filters all nodes which have a degree below the given threshold """filters all nodes which are within the provided bounds by their degree
Parameters Parameters
---------- ----------
threshold : int bound_lower : int | None
nodes with a degree smaller than this value will be removed lower bound for node degree, nodes with degree equal to this value are retained
bound_upper : int | None
upper bound for node degree, nodes with degree equal to this value are retained
Returns Returns
------- -------
@ -338,9 +349,15 @@ class TokenGraph(DiGraph):
original_graph_nodes = copy.deepcopy(self.nodes) original_graph_nodes = copy.deepcopy(self.nodes)
filtered_graph = self.copy() filtered_graph = self.copy()
if not any([bound_lower, bound_upper]):
logger.warning('No bounds provided, returning original graph.')
return filtered_graph
for node in original_graph_nodes: for node in original_graph_nodes:
degree = filtered_graph.degree[node] # type: ignore degree = filtered_graph.degree[node] # type: ignore
if degree < threshold: if bound_lower is not None and degree < bound_lower:
filtered_graph.remove_node(node)
if bound_upper is not None and degree > bound_upper:
filtered_graph.remove_node(node) filtered_graph.remove_node(node)
if filtered_graph._undirected is not None: if filtered_graph._undirected is not None:

View File

@ -21,7 +21,7 @@ class STFRDeviceTypes(enum.StrEnum):
GPU = 'cuda' GPU = 'cuda'
# ** datatsets # ** datasets
PandasIndex: TypeAlias = int | np.int64 PandasIndex: TypeAlias = int | np.int64
ObjectID: TypeAlias = int ObjectID: TypeAlias = int
Embedding: TypeAlias = SpacyDoc | Tensor Embedding: TypeAlias = SpacyDoc | Tensor