Skip to content

Commit 357bc77

Browse files
authored
Merge pull request #165 from lucasimi/feature/finalize-pyvis-plot
Feature/finalize pyvis plot
2 parents 90bc2b4 + 03e6be3 commit 357bc77

3 files changed

Lines changed: 155 additions & 38 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ For further details, please refer to the
2626

2727
- **Interactive App**: Interactive tool for quick, in-depth data exploration.
2828

29-
## Background
29+
### Background
3030

3131
The Mapper algorithm is a well-known technique in the field of topological
3232
data analysis that allows data to be represented as a graph.
@@ -43,7 +43,7 @@ read
4343
| ![Step 1](https://github.com/lucasimi/tda-mapper-python/raw/main/resources/mapper_1.png) | ![Step 2](https://github.com/lucasimi/tda-mapper-python/raw/main/resources/mapper_2.png) | ![Step 3](https://github.com/lucasimi/tda-mapper-python/raw/main/resources/mapper_3.png) | ![Step 2](https://github.com/lucasimi/tda-mapper-python/raw/main/resources/mapper_4.png) |
4444
| Chose lens | Cover image | Run clustering | Build graph |
4545

46-
## Citations
46+
### Citations
4747

4848
If you use **tda-mapper** in your work, please consider citing both the
4949
[library](https://doi.org/10.5281/zenodo.10642381), archived in a permanent
@@ -100,7 +100,7 @@ fig.show(config={'scrollZoom': True})
100100
More examples can be found in the
101101
[documentation](https://tda-mapper.readthedocs.io/en/main/examples.html).
102102

103-
## Interactive App
103+
### Interactive App
104104

105105
You can explore a live demo of **tda-mapper** directly on
106106
[Streamlit Cloud](https://tda-mapper-app.streamlit.app/),

src/tdamapper/_plot_pyvis.py

Lines changed: 152 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,139 @@
22
This module provides functionalities to visualize the Mapper graph based on
33
pyvis.
44
"""
5+
import math
56

67
from pyvis.network import Network
78

89
import matplotlib.pyplot as plt
910
import matplotlib.colors as mcolors
1011

12+
import plotly.graph_objects as go
13+
import plotly.io as pio
14+
import plotly.colors as pc
15+
1116
from tdamapper.core import (
1217
aggregate_graph,
1318
)
1419

1520

21+
_EDGE_WIDTH = 0.75
22+
23+
_EDGE_COLOR = '#777'
24+
25+
_TICKS_NUM = 10
26+
27+
28+
def __fmt(x, max_len=3):
29+
fmt = f'.{max_len}g'
30+
return f'{x:{fmt}}'
31+
32+
33+
def _colorbar(height, cmap, cmin, cmax, title):
34+
colorbar_fig = go.Figure()
35+
colorbar_fig.add_trace(go.Scatter(
36+
x=[None], y=[None],
37+
mode='markers',
38+
marker=dict(
39+
showscale=True,
40+
reversescale=False,
41+
line_colorscale=cmap,
42+
colorscale=cmap,
43+
cmin=cmin,
44+
cmax=cmax,
45+
colorbar=dict(
46+
showticklabels=True,
47+
outlinewidth=1,
48+
borderwidth=0,
49+
orientation='v',
50+
thickness=20,
51+
thicknessmode='fraction',
52+
xanchor='left',
53+
titleside='right',
54+
tickwidth=1,
55+
tickformat='.2g',
56+
nticks=_TICKS_NUM,
57+
tickmode='auto',
58+
title=title,
59+
),
60+
),
61+
))
62+
colorbar_fig.update_layout(
63+
xaxis=dict(visible=False),
64+
yaxis=dict(visible=False),
65+
margin=dict(l=0, r=100, t=0, b=0),
66+
width=80,
67+
height=height,
68+
plot_bgcolor='rgba(0,0,0,0)',
69+
paper_bgcolor='rgba(0,0,0,0)',
70+
)
71+
return colorbar_fig
72+
73+
74+
def _combine(network, colorbar):
75+
network_html = network.generate_html()
76+
colorbar_html = pio.to_html(
77+
colorbar,
78+
include_plotlyjs='cdn',
79+
full_html=False,
80+
config={
81+
'displayModeBar': False
82+
},
83+
)
84+
combined_html = f"""
85+
<!DOCTYPE html>
86+
<html>
87+
<head>
88+
<title>Network with Colorbar</title>
89+
<style>
90+
body {{
91+
margin: 0;
92+
height: 100vh;
93+
display: flex;
94+
}}
95+
96+
.combined {{
97+
display: flex;
98+
flex-direction: row;
99+
margin: 0;
100+
}}
101+
102+
.network {{
103+
display: flex;
104+
align-items: start;
105+
}}
106+
107+
.colorbar {{
108+
display: flex;
109+
align-items: start;
110+
}}
111+
112+
.network .card {{
113+
border-width: 0;
114+
}}
115+
116+
.network #mynetwork {{
117+
border-width: 0;
118+
}}
119+
</style>
120+
</head>
121+
<body>
122+
<div class="combined">
123+
<div class="network">
124+
{network_html}
125+
</div>
126+
<div class="colorbar">
127+
{colorbar_html}
128+
</div>
129+
</div>
130+
</body>
131+
</html>
132+
"""
133+
return combined_html
134+
135+
16136
def plot_pyvis(
17137
mapper_plot,
18-
notebook,
19138
output_file,
20139
colors,
21140
agg,
@@ -24,39 +143,47 @@ def plot_pyvis(
24143
height,
25144
cmap,
26145
):
27-
net = _compute_net(
146+
net, cmin, cmax = _compute_net(
28147
mapper_plot=mapper_plot,
29148
width=width,
30149
height=height,
31150
colors=colors,
32151
agg=agg,
33152
cmap=cmap,
34-
notebook=notebook,
35153
)
36-
net.show(output_file, notebook=notebook)
154+
colorbar = _colorbar(
155+
height=height,
156+
cmap=cmap,
157+
cmin=cmin,
158+
cmax=cmax,
159+
title=title
160+
)
161+
combined_html = _combine(net, colorbar)
162+
with open(output_file, 'w') as file:
163+
file.write(combined_html)
37164

38165

39166
def _compute_net(
40167
mapper_plot,
41-
notebook,
42168
colors,
43169
agg,
44170
width,
45171
height,
46172
cmap,
47173
):
48174
net = Network(
49-
height=height,
50-
width=width,
175+
height=f'{height}px',
176+
width=f'{width}px',
51177
directed=False,
52-
notebook=notebook,
53-
select_menu=True,
54-
filter_menu=True,
178+
notebook=True,
179+
select_menu=False,
180+
filter_menu=False,
55181
neighborhood_highlight=True,
56182
)
57183
net.toggle_physics(False)
58184
graph = mapper_plot.graph
59185
nodes = graph.nodes
186+
cmap_colorscale = pc.get_colorscale(cmap)
60187

61188
min_node_size = float('inf')
62189
max_node_size = -float('inf')
@@ -81,10 +208,10 @@ def _compute_net(
81208

82209
def _size(node):
83210
if max_node_size == min_node_size:
84-
node_size_norm = 12
211+
node_size_norm = 25.0
85212
else:
86213
node_size = int(nodes[node]['size'])
87-
node_size_norm = (node_size - min_node_size) / (max_node_size - min_node_size) * 25
214+
node_size_norm = 25.0 * math.sqrt(node_size / max_node_size)
88215
return int(round(node_size_norm))
89216

90217
def _color(node):
@@ -93,39 +220,33 @@ def _color(node):
93220
else:
94221
node_color = node_colors[node]
95222
node_color = (node_color - min_node_color) / (max_node_color - min_node_color)
96-
node_color = colormap(node_color)
97-
return mcolors.to_hex(node_color)
98-
99-
def _blend_color(source, target):
100-
if max_node_color == min_node_color:
101-
blend_color = 0.5
102-
else:
103-
source_color = node_colors[source]
104-
source_color = (source_color - min_node_color) / (max_node_color - min_node_color)
105-
target_color = node_colors[target]
106-
target_color = (target_color - min_node_color) / (max_node_color - min_node_color)
107-
blend_color = (source_color + target_color) / 2.0
108-
blend_color = colormap(blend_color)
109-
return mcolors.to_hex(blend_color)
223+
node_color = max(0.0, min(1.0, node_color))
224+
node_color_hex = pc.sample_colorscale(cmap_colorscale, node_color)[0]
225+
return node_color_hex
110226

111227
for node in nodes:
112228
node_id = int(node)
113229
node_size = _size(node)
114230
node_color = _color(node)
231+
node_stats = __fmt(node_colors[node])
232+
node_label = f'color: {node_stats}\nnode: {node_id}\nsize: {node_size}'
115233
node_pos = mapper_plot.positions[node]
116234
net.add_node(
117235
node_id,
118236
label=node_id,
119237
size=node_size,
120238
color=node_color,
121-
x=node_pos[0] * 1000.0,
122-
y=node_pos[1] * 1000.0,
239+
title=node_label,
240+
x=node_pos[0] * width,
241+
y=-node_pos[1] * height,
123242
)
124243

125244
for edge in graph.edges:
126245
source_id = int(edge[0])
127246
target_id = int(edge[1])
128-
edge_color = _blend_color(edge[0], edge[1])
129-
net.add_edge(source_id, target_id, color=edge_color)
247+
edge_color = _EDGE_COLOR
248+
edge_width = _EDGE_WIDTH
249+
edge_width = 1.5
250+
net.add_edge(source_id, target_id, color=edge_color, width=edge_width)
130251

131-
return net
252+
return net, min_node_color, max_node_color

src/tdamapper/plot.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ def plot_plotly_update(
201201

202202
def plot_pyvis(
203203
self,
204-
notebook,
205204
output_file,
206205
colors,
207206
agg=np.nanmean,
@@ -213,8 +212,6 @@ def plot_pyvis(
213212
"""
214213
Draw an interactive HTML plot using PyVis.
215214
216-
:param notebook: Set to true when running inside Jupyter notebooks.
217-
:type notebook: bool
218215
:param output_file: The path where the html file is written.
219216
:type output_file: str
220217
:type colors: array-like of shape (n,) or list-like of size n
@@ -242,7 +239,6 @@ def plot_pyvis(
242239
"""
243240
return plot_pyvis(
244241
self,
245-
notebook=notebook,
246242
output_file=output_file,
247243
colors=colors,
248244
agg=agg,

0 commit comments

Comments
 (0)