SorrelC commited on
Commit
5f6d3ac
·
verified ·
1 Parent(s): 9093322

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -18
app.py CHANGED
@@ -89,14 +89,28 @@ class NetworkGraphBuilder:
89
 
90
  return G
91
 
92
- def create_pyvis_graph(self, G):
93
- """Create interactive PyVis visualisation. Returns (iframe_html, standalone_html)"""
94
  if len(G.nodes) == 0:
95
- return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  # Create PyVis network with dark theme
98
  net = Network(
99
- height="600px",
100
  width="100%",
101
  bgcolor="#1a1a2e",
102
  font_color="white",
@@ -190,14 +204,28 @@ class NetworkGraphBuilder:
190
 
191
  title = '\n'.join(title_lines)
192
 
193
- net.add_node(
194
- node,
195
- label=node,
196
- color=colour,
197
- size=size,
198
- title=title,
199
- font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'}
200
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  # Add edges with relationship labels
203
  for edge in G.edges(data=True):
@@ -218,6 +246,15 @@ class NetworkGraphBuilder:
218
  # Store standalone HTML for export
219
  standalone_html = html
220
 
 
 
 
 
 
 
 
 
 
221
  # Encode as base64 data URI for iframe src
222
  html_bytes = html.encode('utf-8')
223
  b64_html = base64.b64encode(html_bytes).decode('utf-8')
@@ -232,7 +269,7 @@ class NetworkGraphBuilder:
232
  ></iframe>
233
  '''
234
 
235
- return iframe_html, standalone_html
236
 
237
 
238
  def collect_entities_from_records(
@@ -434,7 +471,7 @@ def generate_network_graph(
434
  return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first."
435
 
436
  # Create visualisation
437
- graph_html, standalone_html = builder.create_pyvis_graph(G)
438
 
439
  # Create statistics with colour legend included
440
  stats_html = f'''
@@ -616,11 +653,175 @@ def export_network_graph(
616
  if len(G.nodes) == 0:
617
  return None
618
 
619
- # Create visualisation
620
- _, standalone_html = builder.create_pyvis_graph(G)
621
 
622
- # Save standalone HTML to a temporary file for download
623
  if standalone_html:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  export_file = tempfile.NamedTemporaryFile(
625
  mode='w',
626
  suffix='.html',
@@ -628,7 +829,7 @@ def export_network_graph(
628
  delete=False,
629
  encoding='utf-8'
630
  )
631
- export_file.write(standalone_html)
632
  export_file.close()
633
  return export_file.name
634
 
 
89
 
90
  return G
91
 
92
+ def create_pyvis_graph(self, G, for_export=False):
93
+ """Create interactive PyVis visualisation. Returns (iframe_html, standalone_html, stats_dict)"""
94
  if len(G.nodes) == 0:
95
+ return None, None, None
96
+
97
+ # Potato SVG as base64 data URI (Easter egg for Excellent Boiled Potatoes!)
98
+ potato_svg = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="50" height="50">
99
+ <ellipse cx="50" cy="50" rx="40" ry="30" fill="#C4A484" transform="rotate(-15 50 50)"/>
100
+ <ellipse cx="35" cy="40" rx="4" ry="3" fill="#8B7355"/>
101
+ <ellipse cx="55" cy="35" rx="3" ry="4" fill="#8B7355"/>
102
+ <ellipse cx="65" cy="50" rx="4" ry="3" fill="#8B7355"/>
103
+ <ellipse cx="45" cy="60" rx="3" ry="4" fill="#8B7355"/>
104
+ <ellipse cx="30" cy="55" rx="3" ry="2" fill="#8B7355"/>
105
+ </svg>'''
106
+ potato_data_uri = "data:image/svg+xml;base64," + base64.b64encode(potato_svg.encode()).decode()
107
+
108
+ # Set height based on whether this is for export
109
+ graph_height = "800px" if for_export else "600px"
110
 
111
  # Create PyVis network with dark theme
112
  net = Network(
113
+ height=graph_height,
114
  width="100%",
115
  bgcolor="#1a1a2e",
116
  font_color="white",
 
204
 
205
  title = '\n'.join(title_lines)
206
 
207
+ # Check if this is a potato node (Easter egg!)
208
+ is_potato = 'potato' in node.lower()
209
+
210
+ if is_potato:
211
+ net.add_node(
212
+ node,
213
+ label=node,
214
+ shape="image",
215
+ image=potato_data_uri,
216
+ size=size + 10,
217
+ title=title,
218
+ font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'}
219
+ )
220
+ else:
221
+ net.add_node(
222
+ node,
223
+ label=node,
224
+ color=colour,
225
+ size=size,
226
+ title=title,
227
+ font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'}
228
+ )
229
 
230
  # Add edges with relationship labels
231
  for edge in G.edges(data=True):
 
246
  # Store standalone HTML for export
247
  standalone_html = html
248
 
249
+ # Calculate stats for export
250
+ stats_dict = {
251
+ 'nodes': G.number_of_nodes(),
252
+ 'edges': G.number_of_edges(),
253
+ 'density': nx.density(G) if len(G.edges) > 0 else 0,
254
+ 'avg_degree': sum(dict(G.degree()).values()) / G.number_of_nodes() if G.number_of_nodes() > 0 else 0,
255
+ 'top_nodes': sorted(dict(G.degree()).items(), key=lambda x: x[1], reverse=True)[:3] if G.number_of_nodes() > 0 else []
256
+ }
257
+
258
  # Encode as base64 data URI for iframe src
259
  html_bytes = html.encode('utf-8')
260
  b64_html = base64.b64encode(html_bytes).decode('utf-8')
 
269
  ></iframe>
270
  '''
271
 
272
+ return iframe_html, standalone_html, stats_dict
273
 
274
 
275
  def collect_entities_from_records(
 
471
  return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first."
472
 
473
  # Create visualisation
474
+ graph_html, standalone_html, stats_dict = builder.create_pyvis_graph(G)
475
 
476
  # Create statistics with colour legend included
477
  stats_html = f'''
 
653
  if len(G.nodes) == 0:
654
  return None
655
 
656
+ # Create visualisation with export flag for larger size
657
+ _, standalone_html, stats_dict = builder.create_pyvis_graph(G, for_export=True)
658
 
659
+ # Create enhanced HTML with legend and statistics
660
  if standalone_html:
661
+ # Build statistics HTML
662
+ stats_content = f'''
663
+ <div style="margin-bottom: 20px;">
664
+ <h3 style="margin: 0 0 15px 0; color: #333;">📈 Network Statistics</h3>
665
+ <table style="width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
666
+ <tr style="background: #f8f9fa;">
667
+ <td style="padding: 12px; border-bottom: 1px solid #eee;"><strong>Nodes (Entities)</strong></td>
668
+ <td style="padding: 12px; border-bottom: 1px solid #eee; text-align: right;">{stats_dict['nodes']}</td>
669
+ </tr>
670
+ <tr>
671
+ <td style="padding: 12px; border-bottom: 1px solid #eee;"><strong>Edges (Relationships)</strong></td>
672
+ <td style="padding: 12px; border-bottom: 1px solid #eee; text-align: right;">{stats_dict['edges']}</td>
673
+ </tr>
674
+ <tr style="background: #f8f9fa;">
675
+ <td style="padding: 12px; border-bottom: 1px solid #eee;"><strong>Density</strong></td>
676
+ <td style="padding: 12px; border-bottom: 1px solid #eee; text-align: right;">{stats_dict['density']:.3f}</td>
677
+ </tr>
678
+ <tr>
679
+ <td style="padding: 12px;"><strong>Avg. Connections</strong></td>
680
+ <td style="padding: 12px; text-align: right;">{stats_dict['avg_degree']:.2f}</td>
681
+ </tr>
682
+ </table>
683
+ '''
684
+
685
+ if stats_dict['top_nodes']:
686
+ stats_content += '''
687
+ <div style="margin-top: 15px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
688
+ <strong>Most Connected:</strong>
689
+ <ul style="margin: 10px 0 0 0; padding-left: 20px;">
690
+ '''
691
+ for node, degree in stats_dict['top_nodes']:
692
+ stats_content += f'<li>{node}: {degree} connections</li>'
693
+ stats_content += '</ul></div>'
694
+
695
+ stats_content += '</div>'
696
+
697
+ # Build legend HTML
698
+ legend_html = f'''
699
+ <div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 20px; border-radius: 10px; margin-bottom: 20px;">
700
+ <h3 style="color: white; margin: 0 0 15px 0; font-size: 16px;">🎨 Entity Colour Legend</h3>
701
+ <div style="display: flex; flex-wrap: wrap; gap: 20px;">
702
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
703
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['PERSON']}; display: inline-block; border: 2px solid white;"></span>
704
+ Person
705
+ </span>
706
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
707
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['LOCATION']}; display: inline-block; border: 2px solid white;"></span>
708
+ Location
709
+ </span>
710
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
711
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['EVENT']}; display: inline-block; border: 2px solid white;"></span>
712
+ Event
713
+ </span>
714
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
715
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['ORGANIZATION']}; display: inline-block; border: 2px solid white;"></span>
716
+ Organisation
717
+ </span>
718
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
719
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['DATE']}; display: inline-block; border: 2px solid white;"></span>
720
+ Date
721
+ </span>
722
+ </div>
723
+ <p style="color: white; margin: 15px 0 0 0; font-size: 13px;">
724
+ 🖱️ <strong style="color: white;">Interaction:</strong> Drag nodes • Scroll to zoom • Hover for details
725
+ </p>
726
+ </div>
727
+ '''
728
+
729
+ # Encode the graph HTML as base64 for the iframe
730
+ graph_b64 = base64.b64encode(standalone_html.encode('utf-8')).decode('utf-8')
731
+
732
+ # Create enhanced HTML wrapper
733
+ enhanced_html = f'''<!DOCTYPE html>
734
+ <html>
735
+ <head>
736
+ <meta charset="utf-8">
737
+ <title>Network Graph - Basic Network Explorer</title>
738
+ <style>
739
+ body {{
740
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
741
+ margin: 0;
742
+ padding: 20px;
743
+ background-color: #f5f5f5;
744
+ }}
745
+ .container {{
746
+ max-width: 1600px;
747
+ margin: 0 auto;
748
+ }}
749
+ .header {{
750
+ text-align: center;
751
+ margin-bottom: 20px;
752
+ padding: 20px;
753
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
754
+ border-radius: 12px;
755
+ color: white;
756
+ }}
757
+ .header h1 {{
758
+ margin: 0 0 10px 0;
759
+ }}
760
+ .header p {{
761
+ margin: 0;
762
+ opacity: 0.9;
763
+ }}
764
+ .content {{
765
+ display: flex;
766
+ gap: 20px;
767
+ }}
768
+ .graph-container {{
769
+ flex: 3;
770
+ background: white;
771
+ border-radius: 12px;
772
+ padding: 15px;
773
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
774
+ }}
775
+ .sidebar {{
776
+ flex: 1;
777
+ min-width: 300px;
778
+ }}
779
+ .graph-frame {{
780
+ width: 100%;
781
+ height: 800px;
782
+ border: 2px solid #333;
783
+ border-radius: 8px;
784
+ background-color: #1a1a2e;
785
+ }}
786
+ .footer {{
787
+ text-align: center;
788
+ margin-top: 20px;
789
+ padding: 15px;
790
+ color: #666;
791
+ font-size: 13px;
792
+ }}
793
+ @media (max-width: 1000px) {{
794
+ .content {{
795
+ flex-direction: column;
796
+ }}
797
+ .sidebar {{
798
+ min-width: 100%;
799
+ }}
800
+ }}
801
+ </style>
802
+ </head>
803
+ <body>
804
+ <div class="container">
805
+ <div class="header">
806
+ <h1>🕸️ Network Graph</h1>
807
+ <p>Created with Basic Network Explorer</p>
808
+ </div>
809
+ <div class="content">
810
+ <div class="graph-container">
811
+ <iframe class="graph-frame" src="data:text/html;base64,{graph_b64}"></iframe>
812
+ </div>
813
+ <div class="sidebar">
814
+ {stats_content}
815
+ {legend_html}
816
+ </div>
817
+ </div>
818
+ <div class="footer">
819
+ <p>Exported from <strong>Basic Network Explorer</strong> — Bodleian Libraries (Oxford) Sassoon Research Fellowship</p>
820
+ </div>
821
+ </div>
822
+ </body>
823
+ </html>'''
824
+
825
  export_file = tempfile.NamedTemporaryFile(
826
  mode='w',
827
  suffix='.html',
 
829
  delete=False,
830
  encoding='utf-8'
831
  )
832
+ export_file.write(enhanced_html)
833
  export_file.close()
834
  return export_file.name
835