Skip to content

Commit 4c421f2

Browse files
committed
Add pyvishtml_to_networkx function in utils.py and use it to render the html networks in quarto and streamlit reports
1 parent fb18a9a commit 4c421f2

File tree

4 files changed

+99
-32
lines changed

4 files changed

+99
-32
lines changed

vuegen/quarto_reportview.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import subprocess
33
import report as r
44
from typing import List
5+
import networkx as nx
56
from utils import create_folder
67

78
class QuartoReportView(r.ReportView):
@@ -280,22 +281,25 @@ def _generate_plot_content(self, plot, is_report_static, static_dir: str = STATI
280281
else:
281282
plot_content.append(f"""fig_altair\n```\n""")
282283
elif plot.plot_type == r.PlotType.INTERACTIVE_NETWORK:
283-
network_data = plot.read_network()
284+
networkx_graph = plot.read_network()
285+
if isinstance(networkx_graph, tuple):
286+
# If network_data is a tuple, separate the network and html file path
287+
networkx_graph, html_plot_file = networkx_graph
288+
elif isinstance(networkx_graph, nx.Graph) and not is_report_static:
289+
# Get the pyvis object and create html
290+
pyvis_graph = plot.create_and_save_pyvis_network(networkx_graph, html_plot_file)
291+
292+
# Add number of nodes and edges to the plor conetnt
293+
num_nodes = networkx_graph.number_of_nodes()
294+
num_edges = networkx_graph.number_of_edges()
295+
plot_content.append(f'**Number of nodes:** {num_nodes}\n')
296+
plot_content.append(f'**Number of edges:** {num_edges}\n')
297+
298+
# Add code to generate network depending on the report type
284299
if is_report_static:
285-
plot.save_netwrok_image(G, static_plot_path, "png")
300+
plot.save_netwrok_image(networkx_graph, static_plot_path, "png")
286301
plot_content.append(self._generate_image_content(static_plot_path))
287302
else:
288-
if isinstance(network_data, str) and network_data.endswith('.html'):
289-
# If network_data is the path to an HTML file, just visualize it
290-
html_plot_file = network_data
291-
else:
292-
num_nodes = network_data.number_of_nodes()
293-
num_edges = network_data.number_of_edges()
294-
plot_content.append(f'**Number of nodes:** {num_nodes}\n')
295-
plot_content.append(f'**Number of edges:** {num_edges}\n')
296-
# Get the Network object
297-
net = plot.create_and_save_pyvis_network(network_data, html_plot_file)
298-
299303
plot_content.append(self._generate_plot_code(plot, html_plot_file))
300304
else:
301305
self.report.logger.warning(f"Unsupported plot type: {plot.plot_type}")

vuegen/report.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import json
1212
import matplotlib.pyplot as plt
1313
from pyvis.network import Network
14-
from utils import cyjs2graph
14+
from utils import cyjs_to_networkx, pyvishtml_to_networkx
1515

1616
class ReportType(StrEnum):
1717
STREAMLIT = auto()
@@ -140,7 +140,7 @@ def read_network(self) -> nx.Graph:
140140
NetworkFormat.GML.value_with_dot: nx.read_gml,
141141
NetworkFormat.GRAPHML.value_with_dot: nx.read_graphml,
142142
NetworkFormat.GEXF.value_with_dot: nx.read_gexf,
143-
NetworkFormat.CYJS.value_with_dot: cyjs2graph
143+
NetworkFormat.CYJS.value_with_dot: cyjs_to_networkx
144144
}
145145

146146
# Check if the file exists
@@ -163,7 +163,8 @@ def read_network(self) -> nx.Graph:
163163
try:
164164
# Handle HTML files (for pyvis interactive networks)
165165
if file_extension == NetworkFormat.HTML.value_with_dot:
166-
return self.file_path
166+
G = pyvishtml_to_networkx(self.file_path)
167+
return (G, self.file_path)
167168

168169
# Handle .csv and .txt files with custom delimiters based on the text format (edgelist or adjlist)
169170
if file_extension in [NetworkFormat.CSV.value_with_dot, NetworkFormat.TXT.value_with_dot] and self.csv_network_format:
@@ -232,7 +233,7 @@ def save_netwrok_image(self, G: nx.Graph, output_file: str, format: str, dpi: in
232233

233234
try:
234235
# Draw the graph and save it as an image file
235-
nx.draw(G, with_labels=True)
236+
nx.draw(G, with_labels=False)
236237
plt.savefig(output_file, format=format, dpi=dpi)
237238
plt.clf()
238239
self.logger.info(f"Network image saved successfully at: {output_file}.")
@@ -275,7 +276,6 @@ def create_and_save_pyvis_network(self, G: nx.Graph, output_file: str) -> Networ
275276
node_data = G.nodes[node_id]
276277
node['label'] = node_data.get('name', node_id)
277278
node['font'] = {'size': 12}
278-
# node_data.get('name', node_id)
279279
node['borderWidth'] = 2
280280
node['borderWidthSelected'] = 2.5
281281

vuegen/streamlit_reportview.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -312,19 +312,19 @@ def _generate_plot_content(self, plot, static_dir: str = STATIC_FILES_DIR) -> Li
312312
elif plot.plot_type == r.PlotType.ALTAIR:
313313
plot_content.append(self._generate_plot_code(plot))
314314
elif plot.plot_type == r.PlotType.INTERACTIVE_NETWORK:
315-
network_data = plot.read_network()
316-
if isinstance(network_data, str) and network_data.endswith('.html'):
317-
# If network_data is the path to an HTML file, just visualize it
318-
html_plot_file = network_data
319-
plot_content.append(f"""with open('{html_plot_file}', 'r') as f:
320-
html_data = f.read()""")
315+
networkx_graph = plot.read_network()
316+
if isinstance(networkx_graph, tuple):
317+
# If network_data is a tuple, separate the network and html file path
318+
networkx_graph, html_plot_file = networkx_graph
321319
else:
322-
# Otherwise, create and save a new pyvis network from the graph
320+
# Otherwise, create and save a new pyvis network from the netowrkx graph
323321
html_plot_file = os.path.join(static_dir, f"{plot.title.replace(' ', '_')}.html")
324-
net = plot.create_and_save_pyvis_network(network_data, html_plot_file)
325-
num_nodes = len(net.nodes)
326-
num_edges = len(net.edges)
327-
plot_content.append(f"""with open('{html_plot_file}', 'r') as f:
322+
pyvis_graph = plot.create_and_save_pyvis_network(networkx_graph, html_plot_file)
323+
324+
# Generate network code for visualization
325+
num_nodes = networkx_graph.number_of_nodes()
326+
num_edges = networkx_graph.number_of_edges()
327+
plot_content.append(f"""with open('{html_plot_file}', 'r') as f:
328328
html_data = f.read()
329329
st.markdown(f"<p style='text-align: center; color: black;'> <b>Number of nodes:</b> {num_nodes} </p>", unsafe_allow_html=True)
330330
st.markdown(f"<p style='text-align: center; color: black;'> <b>Number of relationships:</b> {num_edges} </p>", unsafe_allow_html=True)""")

vuegen/utils.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import json
99
from enum import StrEnum
1010
from typing import Type
11+
from bs4 import BeautifulSoup
1112

1213
## CHECKS
1314
def check_path(filepath: str) -> bool:
@@ -151,8 +152,7 @@ def get_args(prog_name: str, others: dict = {}) -> argparse.Namespace:
151152
# Parse arguments
152153
return parser.parse_args()
153154

154-
155-
def cyjs2graph(file_path: str, name: str = "name", ident: str = "id") -> nx.Graph:
155+
def cyjs_to_networkx(file_path: str, name: str = "name", ident: str = "id") -> nx.Graph:
156156
"""
157157
Create a NetworkX graph from a `.cyjs` file in Cytoscape format, including all attributes present in the JSON data.
158158
This function is modified from the `cytoscape_graph` networkx function to handle the 'value' key explicitly and to include
@@ -170,7 +170,7 @@ def cyjs2graph(file_path: str, name: str = "name", ident: str = "id") -> nx.Grap
170170
171171
Returns
172172
-------
173-
G : networkx.Graph
173+
graph : networkx.Graph
174174
The graph created from the Cytoscape JSON data, including all node and edge attributes.
175175
176176
Raises
@@ -236,6 +236,69 @@ def cyjs2graph(file_path: str, name: str = "name", ident: str = "id") -> nx.Grap
236236
except KeyError as e:
237237
raise ValueError(f"Missing required key in data: {e}")
238238

239+
def pyvishtml_to_networkx(html_file: str) -> nx.Graph:
240+
"""
241+
Converts a PyVis HTML file to a NetworkX graph.
242+
243+
Parameters
244+
----------
245+
html_file : str
246+
Path to the PyVis HTML file.
247+
248+
Returns
249+
-------
250+
graph : nx.Graph
251+
NetworkX graph object reconstructed from the PyVis network data.
252+
253+
Raises
254+
------
255+
ValueError
256+
If the HTML file does not contain the expected network data, or if nodes lack 'id' attribute.
257+
"""
258+
# Load the HTML file
259+
with open(html_file, 'r', encoding='utf-8') as f:
260+
soup = BeautifulSoup(f, 'html.parser')
261+
262+
# Extract the network data from the JavaScript objects
263+
script_tag = soup.find('script', text=lambda x: x and 'nodes = new vis.DataSet' in x)
264+
if not script_tag:
265+
raise ValueError("Could not find network data in the provided HTML file.")
266+
267+
# Parse the nodes and edges
268+
script_text = script_tag.string
269+
nodes_json = json.loads(script_text.split('nodes = new vis.DataSet(')[1].split(');')[0])
270+
edges_json = json.loads(script_text.split('edges = new vis.DataSet(')[1].split(');')[0])
271+
272+
# Create a NetworkX graph
273+
graph = nx.Graph()
274+
275+
# Add nodes
276+
for node in nodes_json:
277+
node_id = node.pop('id', None)
278+
if node_id is None:
279+
raise ValueError("Node is missing an 'id' attribute.")
280+
281+
graph.add_node(node_id, **node)
282+
283+
# Add edges
284+
for edge in edges_json:
285+
source = edge.pop('from')
286+
target = edge.pop('to')
287+
graph.add_edge(source, target, **edge)
288+
289+
# Relabel nodes to use 'name' as the identifier, or 'id' if 'name' is unavailable
290+
mapping = {}
291+
for node_id, data in graph.nodes(data=True):
292+
name = data.get('name')
293+
if name:
294+
mapping[node_id] = name
295+
else:
296+
# Fallback to the original ID if no 'name' exists
297+
mapping[node_id] = node_id
298+
299+
graph = nx.relabel_nodes(graph, mapping)
300+
301+
return graph
239302

240303
## CONFIG
241304
def load_yaml_config(file_path: str) -> dict:

0 commit comments

Comments
 (0)