├── README.md ├── node1.csv ├── edge1.csv ├── LICENSE └── app.py /README.md: -------------------------------------------------------------------------------- 1 | # network-visualization 2 | Python network visualization app using NetworkX, Plotly, Dash 3 | -------------------------------------------------------------------------------- /node1.csv: -------------------------------------------------------------------------------- 1 | Account,CustomerName,Type 2 | A0001,Alice,Type1 3 | A0002,Bob,Type1 4 | A0003,Cindy,Type2 5 | A0004,David,Type2 6 | A0005,Elle,Type3 7 | A0006,Steve,Type2 8 | A0007,John,Type1 9 | A0008,Stella,Type2 10 | -------------------------------------------------------------------------------- /edge1.csv: -------------------------------------------------------------------------------- 1 | TransactionAmt,Source,Target,Date 2 | 100,A0001,A0002,1/1/2018 3 | 300,A0001,A0003,5/6/2017 4 | 500,A0001,A0004,10/3/2018 5 | 200,A0005,A0001,12/4/2018 6 | 300,A0001,A0006,8/4/2015 7 | 420,A0007,A0001,1/9/2016 8 | 120,A0001,A0008,7/7/2018 9 | 220,A0002,A0001,3/9/2013 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 jhwang1992 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import dash 4 | import dash_core_components as dcc 5 | import dash_html_components as html 6 | import networkx as nx 7 | import plotly.graph_objs as go 8 | 9 | import pandas as pd 10 | from colour import Color 11 | from datetime import datetime 12 | from textwrap import dedent as d 13 | import json 14 | 15 | # import the css template, and pass the css template into dash 16 | external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 17 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets) 18 | app.title = "Transaction Network" 19 | 20 | YEAR=[2010, 2019] 21 | ACCOUNT="A0001" 22 | 23 | ############################################################################################################################################################## 24 | def network_graph(yearRange, AccountToSearch): 25 | 26 | edge1 = pd.read_csv('edge1.csv') 27 | node1 = pd.read_csv('node1.csv') 28 | 29 | # filter the record by datetime, to enable interactive control through the input box 30 | edge1['Datetime'] = "" # add empty Datetime column to edge1 dataframe 31 | accountSet=set() # contain unique account 32 | for index in range(0,len(edge1)): 33 | edge1['Datetime'][index] = datetime.strptime(edge1['Date'][index], '%d/%m/%Y') 34 | if edge1['Datetime'][index].yearyearRange[1]: 35 | edge1.drop(axis=0, index=index, inplace=True) 36 | continue 37 | accountSet.add(edge1['Source'][index]) 38 | accountSet.add(edge1['Target'][index]) 39 | 40 | # to define the centric point of the networkx layout 41 | shells=[] 42 | shell1=[] 43 | shell1.append(AccountToSearch) 44 | shells.append(shell1) 45 | shell2=[] 46 | for ele in accountSet: 47 | if ele!=AccountToSearch: 48 | shell2.append(ele) 49 | shells.append(shell2) 50 | 51 | 52 | G = nx.from_pandas_edgelist(edge1, 'Source', 'Target', ['Source', 'Target', 'TransactionAmt', 'Date'], create_using=nx.MultiDiGraph()) 53 | nx.set_node_attributes(G, node1.set_index('Account')['CustomerName'].to_dict(), 'CustomerName') 54 | nx.set_node_attributes(G, node1.set_index('Account')['Type'].to_dict(), 'Type') 55 | # pos = nx.layout.spring_layout(G) 56 | # pos = nx.layout.circular_layout(G) 57 | # nx.layout.shell_layout only works for more than 3 nodes 58 | if len(shell2)>1: 59 | pos = nx.drawing.layout.shell_layout(G, shells) 60 | else: 61 | pos = nx.drawing.layout.spring_layout(G) 62 | for node in G.nodes: 63 | G.nodes[node]['pos'] = list(pos[node]) 64 | 65 | 66 | if len(shell2)==0: 67 | traceRecode = [] # contains edge_trace, node_trace, middle_node_trace 68 | 69 | node_trace = go.Scatter(x=tuple([1]), y=tuple([1]), text=tuple([str(AccountToSearch)]), textposition="bottom center", 70 | mode='markers+text', 71 | marker={'size': 50, 'color': 'LightSkyBlue'}) 72 | traceRecode.append(node_trace) 73 | 74 | node_trace1 = go.Scatter(x=tuple([1]), y=tuple([1]), 75 | mode='markers', 76 | marker={'size': 50, 'color': 'LightSkyBlue'}, 77 | opacity=0) 78 | traceRecode.append(node_trace1) 79 | 80 | figure = { 81 | "data": traceRecode, 82 | "layout": go.Layout(title='Interactive Transaction Visualization', showlegend=False, 83 | margin={'b': 40, 'l': 40, 'r': 40, 't': 40}, 84 | xaxis={'showgrid': False, 'zeroline': False, 'showticklabels': False}, 85 | yaxis={'showgrid': False, 'zeroline': False, 'showticklabels': False}, 86 | height=600 87 | )} 88 | return figure 89 | 90 | 91 | traceRecode = [] # contains edge_trace, node_trace, middle_node_trace 92 | ############################################################################################################################################################ 93 | colors = list(Color('lightcoral').range_to(Color('darkred'), len(G.edges()))) 94 | colors = ['rgb' + str(x.rgb) for x in colors] 95 | 96 | index = 0 97 | for edge in G.edges: 98 | x0, y0 = G.nodes[edge[0]]['pos'] 99 | x1, y1 = G.nodes[edge[1]]['pos'] 100 | weight = float(G.edges[edge]['TransactionAmt']) / max(edge1['TransactionAmt']) * 10 101 | trace = go.Scatter(x=tuple([x0, x1, None]), y=tuple([y0, y1, None]), 102 | mode='lines', 103 | line={'width': weight}, 104 | marker=dict(color=colors[index]), 105 | line_shape='spline', 106 | opacity=1) 107 | traceRecode.append(trace) 108 | index = index + 1 109 | ############################################################################################################################################################### 110 | node_trace = go.Scatter(x=[], y=[], hovertext=[], text=[], mode='markers+text', textposition="bottom center", 111 | hoverinfo="text", marker={'size': 50, 'color': 'LightSkyBlue'}) 112 | 113 | index = 0 114 | for node in G.nodes(): 115 | x, y = G.nodes[node]['pos'] 116 | hovertext = "CustomerName: " + str(G.nodes[node]['CustomerName']) + "
" + "AccountType: " + str( 117 | G.nodes[node]['Type']) 118 | text = node1['Account'][index] 119 | node_trace['x'] += tuple([x]) 120 | node_trace['y'] += tuple([y]) 121 | node_trace['hovertext'] += tuple([hovertext]) 122 | node_trace['text'] += tuple([text]) 123 | index = index + 1 124 | 125 | traceRecode.append(node_trace) 126 | ################################################################################################################################################################ 127 | middle_hover_trace = go.Scatter(x=[], y=[], hovertext=[], mode='markers', hoverinfo="text", 128 | marker={'size': 20, 'color': 'LightSkyBlue'}, 129 | opacity=0) 130 | 131 | index = 0 132 | for edge in G.edges: 133 | x0, y0 = G.nodes[edge[0]]['pos'] 134 | x1, y1 = G.nodes[edge[1]]['pos'] 135 | hovertext = "From: " + str(G.edges[edge]['Source']) + "
" + "To: " + str( 136 | G.edges[edge]['Target']) + "
" + "TransactionAmt: " + str( 137 | G.edges[edge]['TransactionAmt']) + "
" + "TransactionDate: " + str(G.edges[edge]['Date']) 138 | middle_hover_trace['x'] += tuple([(x0 + x1) / 2]) 139 | middle_hover_trace['y'] += tuple([(y0 + y1) / 2]) 140 | middle_hover_trace['hovertext'] += tuple([hovertext]) 141 | index = index + 1 142 | 143 | traceRecode.append(middle_hover_trace) 144 | ################################################################################################################################################################# 145 | figure = { 146 | "data": traceRecode, 147 | "layout": go.Layout(title='Interactive Transaction Visualization', showlegend=False, hovermode='closest', 148 | margin={'b': 40, 'l': 40, 'r': 40, 't': 40}, 149 | xaxis={'showgrid': False, 'zeroline': False, 'showticklabels': False}, 150 | yaxis={'showgrid': False, 'zeroline': False, 'showticklabels': False}, 151 | height=600, 152 | clickmode='event+select', 153 | annotations=[ 154 | dict( 155 | ax=(G.nodes[edge[0]]['pos'][0] + G.nodes[edge[1]]['pos'][0]) / 2, 156 | ay=(G.nodes[edge[0]]['pos'][1] + G.nodes[edge[1]]['pos'][1]) / 2, axref='x', ayref='y', 157 | x=(G.nodes[edge[1]]['pos'][0] * 3 + G.nodes[edge[0]]['pos'][0]) / 4, 158 | y=(G.nodes[edge[1]]['pos'][1] * 3 + G.nodes[edge[0]]['pos'][1]) / 4, xref='x', yref='y', 159 | showarrow=True, 160 | arrowhead=3, 161 | arrowsize=4, 162 | arrowwidth=1, 163 | opacity=1 164 | ) for edge in G.edges] 165 | )} 166 | return figure 167 | ###################################################################################################################################################################### 168 | # styles: for right side hover/click component 169 | styles = { 170 | 'pre': { 171 | 'border': 'thin lightgrey solid', 172 | 'overflowX': 'scroll' 173 | } 174 | } 175 | 176 | app.layout = html.Div([ 177 | #########################Title 178 | html.Div([html.H1("Transaction Network Graph")], 179 | className="row", 180 | style={'textAlign': "center"}), 181 | #############################################################################################define the row 182 | html.Div( 183 | className="row", 184 | children=[ 185 | ##############################################left side two input components 186 | html.Div( 187 | className="two columns", 188 | children=[ 189 | dcc.Markdown(d(""" 190 | **Time Range To Visualize** 191 | 192 | Slide the bar to define year range. 193 | """)), 194 | html.Div( 195 | className="twelve columns", 196 | children=[ 197 | dcc.RangeSlider( 198 | id='my-range-slider', 199 | min=2010, 200 | max=2019, 201 | step=1, 202 | value=[2010, 2019], 203 | marks={ 204 | 2010: {'label': '2010'}, 205 | 2011: {'label': '2011'}, 206 | 2012: {'label': '2012'}, 207 | 2013: {'label': '2013'}, 208 | 2014: {'label': '2014'}, 209 | 2015: {'label': '2015'}, 210 | 2016: {'label': '2016'}, 211 | 2017: {'label': '2017'}, 212 | 2018: {'label': '2018'}, 213 | 2019: {'label': '2019'} 214 | } 215 | ), 216 | html.Br(), 217 | html.Div(id='output-container-range-slider') 218 | ], 219 | style={'height': '300px'} 220 | ), 221 | html.Div( 222 | className="twelve columns", 223 | children=[ 224 | dcc.Markdown(d(""" 225 | **Account To Search** 226 | 227 | Input the account to visualize. 228 | """)), 229 | dcc.Input(id="input1", type="text", placeholder="Account"), 230 | html.Div(id="output") 231 | ], 232 | style={'height': '300px'} 233 | ) 234 | ] 235 | ), 236 | 237 | ############################################middle graph component 238 | html.Div( 239 | className="eight columns", 240 | children=[dcc.Graph(id="my-graph", 241 | figure=network_graph(YEAR, ACCOUNT))], 242 | ), 243 | 244 | #########################################right side two output component 245 | html.Div( 246 | className="two columns", 247 | children=[ 248 | html.Div( 249 | className='twelve columns', 250 | children=[ 251 | dcc.Markdown(d(""" 252 | **Hover Data** 253 | 254 | Mouse over values in the graph. 255 | """)), 256 | html.Pre(id='hover-data', style=styles['pre']) 257 | ], 258 | style={'height': '400px'}), 259 | 260 | html.Div( 261 | className='twelve columns', 262 | children=[ 263 | dcc.Markdown(d(""" 264 | **Click Data** 265 | 266 | Click on points in the graph. 267 | """)), 268 | html.Pre(id='click-data', style=styles['pre']) 269 | ], 270 | style={'height': '400px'}) 271 | ] 272 | ) 273 | ] 274 | ) 275 | ]) 276 | 277 | ###################################callback for left side components 278 | @app.callback( 279 | dash.dependencies.Output('my-graph', 'figure'), 280 | [dash.dependencies.Input('my-range-slider', 'value'), dash.dependencies.Input('input1', 'value')]) 281 | def update_output(value,input1): 282 | YEAR = value 283 | ACCOUNT = input1 284 | return network_graph(value, input1) 285 | # to update the global variable of YEAR and ACCOUNT 286 | ################################callback for right side components 287 | @app.callback( 288 | dash.dependencies.Output('hover-data', 'children'), 289 | [dash.dependencies.Input('my-graph', 'hoverData')]) 290 | def display_hover_data(hoverData): 291 | return json.dumps(hoverData, indent=2) 292 | 293 | 294 | @app.callback( 295 | dash.dependencies.Output('click-data', 'children'), 296 | [dash.dependencies.Input('my-graph', 'clickData')]) 297 | def display_click_data(clickData): 298 | return json.dumps(clickData, indent=2) 299 | 300 | 301 | 302 | if __name__ == '__main__': 303 | app.run_server(debug=True) 304 | --------------------------------------------------------------------------------