Skip to content

Commit

Permalink
size feature added; closes #8
Browse files Browse the repository at this point in the history
  • Loading branch information
imohitmayank committed Jul 5, 2021
1 parent 2b1e469 commit e2fa9f7
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 128 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ At present, the dashboard consist of following sections,
### 3. Coloring
<img src="jaal/assest/jaal_color.gif" alt="dashboard"/>

### 4. Size
<img src="jaal/assest/jaal_size.gif" alt="dashboard"/>

## 👉 Extra settings

### Display edge label
Expand Down
Binary file added jaal/assest/jaal_size.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
160 changes: 80 additions & 80 deletions jaal/datasets/got/got_node_df.csv
Original file line number Diff line number Diff line change
@@ -1,80 +1,80 @@
id,gender
Illyrio-Mopatis,male
Jory-Cassel,male
Viserys-Targaryen,male
Mirri-Maz-Duur,female
Jhogo,male
Halder,male
Jeor-Mormont,male
Robert-Baratheon,male
Jaremy-Rykker,male
Robb-Stark,male
Theon-Greyjoy,male
Pypar,male
Pycelle,male
Hallis-Mollen,male
Vayon-Poole,male
Bronn,male
Eddard-Stark,male
Qotho,male
Drogo,male
Arya-Stark,female
Bran-Stark,male
Jon-Snow,male
Syrio-Forel,male
Loras-Tyrell,male
Gared,male
Haggo,male
Aggo,male
Rodrik-Cassel,male
Jhiqui,female
Brynden-Tully,male
Gregor-Clegane,male
Benjen-Stark,male
Alyn,female
Aerys-II-Targaryen,male
Vardis-Egen,male
Walder-Frey,male
Osha,female
Ogo,male
Stannis-Baratheon,male
Jaime-Lannister,male
Mord,male
Cersei-Lannister,female
Todder,male
Jon-Arryn,male
Jeyne-Poole,female
Sandor-Clegane,male
Rickon-Stark,male
Kevan-Lannister,male
Barristan-Selmy,male
Shae,female
Petyr-Baelish,male
Hodor,male
Tyrion-Lannister,male
Will-(prologue),male
Tomard,male
Mycah,male
Alliser-Thorne,male
Samwell-Tarly,male
Joffrey-Baratheon,male
Aemon-Targaryen-(Maester-Aemon),male
Tywin-Lannister,male
Daenerys-Targaryen,female
Catelyn-Stark,female
Irri,female
Renly-Baratheon,male
Lysa-Arryn,female
Mordane,female
Waymar-Royce,male
Lyanna-Stark,female
Doreah,female
Rhaegar-Targaryen,male
Grenn,male
Jorah-Mormont,male
Sansa-Stark,female
Luwin,male
Varys,male
Myrcella-Baratheon,female
Shagga,male
Brandon-Stark,male
id,gender,screentime
Illyrio-Mopatis,male,3.3
Jory-Cassel,male,6.15
Viserys-Targaryen,male,20.3
Mirri-Maz-Duur,female,7.3
Jhogo,male,1
Halder,male,1
Jeor-Mormont,male,21
Robert-Baratheon,male,30.3
Jaremy-Rykker,male,1
Robb-Stark,male,77.45
Theon-Greyjoy,male,123.3
Pypar,male,12
Pycelle,male,1
Hallis-Mollen,male,1
Vayon-Poole,male,1
Bronn,male,64
Eddard-Stark,male,99.45
Qotho,male,5.15
Drogo,male,25
Arya-Stark,female,189.15
Bran-Stark,male,86
Jon-Snow,male,268.15
Syrio-Forel,male,7.15
Loras-Tyrell,male,33
Gared,male,2.3
Haggo,male,1
Aggo,male,1
Rodrik-Cassel,male,12.45
Jhiqui,female,1
Brynden-Tully,male,1
Gregor-Clegane,male,17.15
Benjen-Stark,male,12
Alyn,female,1
Aerys-II-Targaryen,male,1
Vardis-Egen,male,3
Walder-Frey,male,15.45
Osha,female,29.45
Ogo,male,1
Stannis-Baratheon,male,73.15
Jaime-Lannister,male,162.3
Mord,male,2.45
Cersei-Lannister,female,201.45
Todder,male,1
Jon-Arryn,male,1
Jeyne-Poole,female,1
Sandor-Clegane,male,1
Rickon-Stark,male,12.45
Kevan-Lannister,male,8.3
Barristan-Selmy,male,37.15
Shae,female,47.15
Petyr-Baelish,male,1
Hodor,male,29
Tyrion-Lannister,male,293.3
Will-(prologue),male,1
Tomard,male,1
Mycah,male,1
Alliser-Thorne,male,26
Samwell-Tarly,male,121.45
Joffrey-Baratheon,male,70.15
Aemon-Targaryen-(Maester-Aemon),male,1
Tywin-Lannister,male,78.15
Daenerys-Targaryen,female,221.3
Catelyn-Stark,female,82.45
Irri,female,10
Renly-Baratheon,male,24
Lysa-Arryn,female,16.3
Mordane,female,1
Waymar-Royce,male,2.45
Lyanna-Stark,female,1
Doreah,female,12.3
Rhaegar-Targaryen,male,1
Grenn,male,21.15
Jorah-Mormont,male,117.3
Sansa-Stark,female,199.3
Luwin,male,1
Varys,male,1
Myrcella-Baratheon,female,11.45
Shagga,male,5
Brandon-Stark,male,1
25 changes: 23 additions & 2 deletions jaal/datasets/parse_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
Parse network data from dataframe format into visdcc format
"""

def compute_scaling_vars_for_numerical_cols(df):
"""Identify and scale numerical cols"""
# identify numerical cols
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
numeric_cols = df.select_dtypes(include=numerics).columns.tolist()
# var to hold the scaling function
scaling_vars = {}
# scale numerical cols
for col in numeric_cols:
minn, maxx = df[col].min(), df[col].max()
scaling_vars[col] = {'min': minn, 'max': maxx}
# return
return scaling_vars

def parse_dataframe(edge_df, node_df=None):
"""Parse the network dataframe into visdcc format
Expand All @@ -26,6 +41,12 @@ def parse_dataframe(edge_df, node_df=None):
# Data post processing - convert the from and to columns in edge data as string for searching
edge_df.loc[:, ['from', 'to']] = edge_df.loc[:, ['from', 'to']].astype(str)

# Data pot processing (scaling numerical cols in nodes and edge)
scaling_vars = {'node': None, 'edge': None}
if node_df is not None:
scaling_vars['node'] = compute_scaling_vars_for_numerical_cols(node_df)
scaling_vars['edge'] = compute_scaling_vars_for_numerical_cols(edge_df)

# create node list w.r.t. the presence of absence of node_df
nodes = []
if node_df is None:
Expand All @@ -41,7 +62,7 @@ def parse_dataframe(edge_df, node_df=None):
# create edges from df
edges = []
for row in edge_df.to_dict(orient='records'):
edges.append({**row, **{'id': row['from'] + "__" + row['to'], 'color': {'color': '#97C2FC'}}})
edges.append({**row, **{'id': row['from'] + "__" + row['to'], 'color': {'color': '#97C2FC'}}})

# return
return {'nodes': nodes, 'edges': edges}
return {'nodes': nodes, 'edges': edges}, scaling_vars
105 changes: 97 additions & 8 deletions jaal/jaal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output, State
from .datasets.parse_dataframe import parse_dataframe
from .layout import get_app_layout, get_distinct_colors, create_color_legend, DEFAULT_COLOR
from .layout import get_app_layout, get_distinct_colors, create_color_legend, DEFAULT_COLOR, DEFAULT_NODE_SIZE, DEFAULT_EDGE_SIZE

# class
class Jaal:
Expand All @@ -30,26 +30,26 @@ def __init__(self, edge_df, node_df=None):
The network node data stored in format of pandas dataframe
"""
print("Parsing the data...", end="")
self.data = parse_dataframe(edge_df, node_df)
self.data, self.scaling_vars = parse_dataframe(edge_df, node_df)
self.filtered_data = self.data.copy()
self.node_value_color_mapping = {}
self.edge_value_color_mapping = {}
print("Done")

def _callback_search_graph(self, graph_data, search_text):
"""Highlight the nodes which match the search text
"""Only show the nodes which match the search text
"""
nodes = graph_data['nodes']
for node in nodes:
if search_text not in node['label'].lower():
node['color'] = '#f4f8fe'
node['hidden'] = True
else:
node['color'] = DEFAULT_COLOR
node['hidden'] = False
graph_data['nodes'] = nodes
return graph_data

def _callback_filter_nodes(self, graph_data, filter_nodes_text):
"""
"""Filter the nodes based on the Python query syntax
"""
self.filtered_data = self.data.copy()
node_df = pd.DataFrame(self.filtered_data['nodes'])
Expand All @@ -67,6 +67,8 @@ def _callback_filter_nodes(self, graph_data, filter_nodes_text):
return graph_data

def _callback_filter_edges(self, graph_data, filter_edges_text):
"""Filter the edges based on the Python query syntax
"""
self.filtered_data = self.data.copy()
edges_df = pd.DataFrame(self.filtered_data['edges'])
try:
Expand Down Expand Up @@ -101,6 +103,29 @@ def _callback_color_nodes(self, graph_data, color_nodes_value):
self.filtered_data['nodes'] = [x for x in self.data['nodes'] if x['id'] in filtered_nodes]
graph_data = self.filtered_data
return graph_data, value_color_mapping

def _callback_size_nodes(self, graph_data, size_nodes_value):

# color option is None, revert back all changes
if size_nodes_value == 'None':
# revert to default color
for node in self.data['nodes']:
node['size'] = DEFAULT_NODE_SIZE
else:
print("Modifying node size using ", size_nodes_value)
# fetch the scaling value
minn = self.scaling_vars['node'][size_nodes_value]['min']
maxx = self.scaling_vars['node'][size_nodes_value]['max']
# define the scaling function
scale_val = lambda x: 20*(x-minn)/(maxx-minn)
# set size after scaling
for node in self.data['nodes']:
node['size'] = node['size'] + scale_val(node[size_nodes_value])
# filter the data currently shown
filtered_nodes = [x['id'] for x in self.filtered_data['nodes']]
self.filtered_data['nodes'] = [x for x in self.data['nodes'] if x['id'] in filtered_nodes]
graph_data = self.filtered_data
return graph_data

def _callback_color_edges(self, graph_data, color_edges_value):
value_color_mapping = {}
Expand All @@ -122,6 +147,28 @@ def _callback_color_edges(self, graph_data, color_edges_value):
graph_data = self.filtered_data
return graph_data, value_color_mapping

def _callback_size_edges(self, graph_data, size_edges_value):
# color option is None, revert back all changes
if size_edges_value == 'None':
# revert to default color
for edge in self.data['edges']:
edge['width'] = DEFAULT_EDGE_SIZE
else:
print("Modifying edge size using ", size_edges_value)
# fetch the scaling value
minn = self.scaling_vars['edge'][size_edges_value]['min']
maxx = self.scaling_vars['edge'][size_edges_value]['max']
# define the scaling function
scale_val = lambda x: 20*(x-minn)/(maxx-minn)
# set the size after scaling
for edge in self.data['edges']:
edge['width'] = scale_val(edge[size_edges_value])
# filter the data currently shown
filtered_edges = [x['id'] for x in self.filtered_data['edges']]
self.filtered_data['edges'] = [x for x in self.data['edges'] if x['id'] in filtered_edges]
graph_data = self.filtered_data
return graph_data

def get_color_popover_legend_children(self, node_value_color_mapping={}, edge_value_color_mapping={}):
"""Get the popover legends for node and edge based on the color setting
"""
Expand Down Expand Up @@ -185,17 +232,53 @@ def toggle_popover(n, is_open):
return not is_open
return is_open

# create callbacks to toggle hide/show sections - FILTER section
@app.callback(
Output("filter-show-toggle", "is_open"),
[Input("filter-show-toggle-button", "n_clicks")],
[State("filter-show-toggle", "is_open")],
)
def toggle_filter_collapse(n, is_open):
if n:
return not is_open
return is_open

# create callbacks to toggle hide/show sections - COLOR section
@app.callback(
Output("color-show-toggle", "is_open"),
[Input("color-show-toggle-button", "n_clicks")],
[State("color-show-toggle", "is_open")],
)
def toggle_filter_collapse(n, is_open):
if n:
return not is_open
return is_open

# create callbacks to toggle hide/show sections - COLOR section
@app.callback(
Output("size-show-toggle", "is_open"),
[Input("size-show-toggle-button", "n_clicks")],
[State("size-show-toggle", "is_open")],
)
def toggle_filter_collapse(n, is_open):
if n:
return not is_open
return is_open

# create the main callbacks
@app.callback(
[Output('graph', 'data'), Output('color-legend-popup', 'children')],
[Input('search_graph', 'value'),
Input('filter_nodes', 'value'),
Input('filter_edges', 'value'),
Input('color_nodes', 'value'),
Input('color_edges', 'value')],
Input('color_edges', 'value'),
Input('size_nodes', 'value'),
Input('size_edges', 'value')],
state=State('graph', 'data')
)
def setting_pane_callback(search_text, filter_nodes_text, filter_edges_text, color_nodes_value, color_edges_value, graph_data):
def setting_pane_callback(search_text, filter_nodes_text, filter_edges_text,
color_nodes_value, color_edges_value, size_nodes_value, size_edges_value, graph_data):
# fetch the id of option which triggered
ctx = dash.callback_context
# if its the first call
Expand All @@ -220,6 +303,12 @@ def setting_pane_callback(search_text, filter_nodes_text, filter_edges_text, col
# If color edge text is provided
if input_id == 'color_edges':
graph_data, self.edge_value_color_mapping = self._callback_color_edges(graph_data, color_edges_value)
# If size node text is provided
if input_id == 'size_nodes':
graph_data = self._callback_size_nodes(graph_data, size_nodes_value)
# If size edge text is provided
if input_id == 'size_edges':
graph_data = self._callback_size_edges(graph_data, size_edges_value)
# create the color legend childrens
color_popover_legend_children = self.get_color_popover_legend_children(self.node_value_color_mapping, self.edge_value_color_mapping)
# finally return the modified data
Expand Down
Loading

0 comments on commit e2fa9f7

Please sign in to comment.