22import plotly .graph_objs as go
33from nicegui import run , ui
44from sklearn .cluster import DBSCAN , AgglomerativeClustering , KMeans
5- from sklearn .datasets import load_digits
5+ from sklearn .datasets import load_digits , load_iris
66from sklearn .decomposition import PCA
77from umap import UMAP
88
@@ -55,9 +55,63 @@ def _func(X):
5555CLUSTERING_AGGLOMERATIVE = "Agglomerative"
5656CLUSTERING_DBSCAN = "DBSCAN"
5757
58+ DATA_SOURCE_EXAMPLE = "Example"
59+ DATA_SOURCE_CSV = "CSV"
60+ DATA_SOURCE_OPENML = "OpenML"
61+
62+ DATA_SOURCE_EXAMPLE_DIGITS = "Digits"
63+ DATA_SOURCE_EXAMPLE_IRIS = "Iris"
64+
65+ DRAW_3D = "3D"
66+ DRAW_2D = "2D"
67+ DRAW_ITERATIONS = 50
68+
5869
5970class App :
6071
72+ def build_dataset (self ):
73+ self .data_source_type = ui .select (
74+ label = "Data Source" ,
75+ options = [
76+ DATA_SOURCE_EXAMPLE ,
77+ DATA_SOURCE_CSV ,
78+ DATA_SOURCE_OPENML ,
79+ ],
80+ value = DATA_SOURCE_EXAMPLE ,
81+ on_change = self .update_dataset_handler ,
82+ ).classes ("w-full" )
83+ self .data_source_example_file = ui .select (
84+ label = "File" ,
85+ options = [
86+ DATA_SOURCE_EXAMPLE_DIGITS ,
87+ DATA_SOURCE_EXAMPLE_IRIS ,
88+ ],
89+ value = DATA_SOURCE_EXAMPLE_DIGITS ,
90+ on_change = self .update_dataset_handler ,
91+ ).classes ("w-full" )
92+ self .data_source_example_file .bind_visibility_from (
93+ target_object = self .data_source_type ,
94+ target_name = "value" ,
95+ value = DATA_SOURCE_EXAMPLE ,
96+ )
97+ self .data_source_csv = ui .upload (
98+ on_upload = self .update_dataset_handler ,
99+ ).classes ("w-full" )
100+ self .data_source_csv .bind_visibility_from (
101+ target_object = self .data_source_type ,
102+ target_name = "value" ,
103+ value = DATA_SOURCE_CSV ,
104+ )
105+ self .data_source_openml = ui .input (
106+ label = "OpenML Code" ,
107+ on_change = self .update_dataset_handler ,
108+ ).classes ("w-full" )
109+ self .data_source_openml .bind_visibility_from (
110+ target_object = self .data_source_type ,
111+ target_name = "value" ,
112+ value = DATA_SOURCE_OPENML ,
113+ )
114+
61115 def build_lens (self ):
62116 self .lens_type = ui .select (
63117 label = "Lens type" ,
@@ -67,14 +121,14 @@ def build_lens(self):
67121 LENS_UMAP ,
68122 ],
69123 value = LENS_PCA ,
70- on_change = self .update_handler ,
124+ on_change = self .update_graph_handler ,
71125 ).classes ("w-full" )
72126 self .pca_n_components = ui .number (
73127 label = "PCA Components" ,
74128 min = 1 ,
75129 max = 10 ,
76130 value = 2 ,
77- on_change = self .update_handler ,
131+ on_change = self .update_graph_handler ,
78132 ).classes ("w-full" )
79133 self .pca_n_components .bind_visibility_from (
80134 target_object = self .lens_type ,
@@ -86,7 +140,7 @@ def build_lens(self):
86140 min = 1 ,
87141 max = 10 ,
88142 value = 2 ,
89- on_change = self .update_handler ,
143+ on_change = self .update_graph_handler ,
90144 ).classes ("w-full" )
91145 self .umap_n_components .bind_visibility_from (
92146 target_object = self .lens_type ,
@@ -95,7 +149,6 @@ def build_lens(self):
95149 )
96150
97151 def build_cover (self ):
98-
99152 self .cover_type = ui .select (
100153 label = "Cover type" ,
101154 options = [
@@ -105,14 +158,14 @@ def build_cover(self):
105158 COVER_KNN ,
106159 ],
107160 value = COVER_CUBICAL ,
108- on_change = self .update_handler ,
161+ on_change = self .update_graph_handler ,
109162 ).classes ("w-full" )
110163 self .cover_cubical_n_intervals = ui .number (
111164 label = "Intervals" ,
112165 min = 1 ,
113166 max = 100 ,
114167 value = 2 ,
115- on_change = self .update_handler ,
168+ on_change = self .update_graph_handler ,
116169 ).classes ("w-full" )
117170 self .cover_cubical_n_intervals .bind_visibility_from (
118171 target_object = self .cover_type ,
@@ -122,9 +175,9 @@ def build_cover(self):
122175 self .cover_cubical_overlap_frac = ui .number (
123176 label = "Overlap" ,
124177 min = 0.0 ,
125- max = 1.0 ,
126- value = 0.5 ,
127- on_change = self .update_handler ,
178+ max = 0.5 ,
179+ value = 0.25 ,
180+ on_change = self .update_graph_handler ,
128181 ).classes ("w-full" )
129182 self .cover_cubical_overlap_frac .bind_visibility_from (
130183 target_object = self .cover_type ,
@@ -135,7 +188,7 @@ def build_cover(self):
135188 label = "Radius" ,
136189 min = 0.0 ,
137190 value = 100.0 ,
138- on_change = self .update_handler ,
191+ on_change = self .update_graph_handler ,
139192 ).classes ("w-full" )
140193 self .cover_ball_radius .bind_visibility_from (
141194 target_object = self .cover_type ,
@@ -146,7 +199,7 @@ def build_cover(self):
146199 label = "Neighbors" ,
147200 min = 0 ,
148201 value = 10 ,
149- on_change = self .update_handler ,
202+ on_change = self .update_graph_handler ,
150203 ).classes ("w-full" )
151204 self .cover_knn_neighbors .bind_visibility_from (
152205 target_object = self .cover_type ,
@@ -164,13 +217,13 @@ def build_clustering(self):
164217 CLUSTERING_DBSCAN ,
165218 ],
166219 value = CLUSTERING_TRIVIAL ,
167- on_change = self .update_handler ,
220+ on_change = self .update_graph_handler ,
168221 ).classes ("w-full" )
169222 self .clustering_kmeans_n_clusters = ui .number (
170223 label = "Clusters" ,
171224 min = 1 ,
172225 value = 2 ,
173- on_change = self .update_handler ,
226+ on_change = self .update_graph_handler ,
174227 ).classes ("w-full" )
175228 self .clustering_kmeans_n_clusters .bind_visibility_from (
176229 target_object = self .clustering_type ,
@@ -181,7 +234,7 @@ def build_clustering(self):
181234 label = "Eps" ,
182235 min = 0.0 ,
183236 value = 0.5 ,
184- on_change = self .update_handler ,
237+ on_change = self .update_graph_handler ,
185238 ).classes ("w-full" )
186239 self .clustering_dbscan_eps .bind_visibility_from (
187240 target_object = self .clustering_type ,
@@ -192,7 +245,7 @@ def build_clustering(self):
192245 label = "Min Samples" ,
193246 min = 1 ,
194247 value = 5 ,
195- on_change = self .update_handler ,
248+ on_change = self .update_graph_handler ,
196249 ).classes ("w-full" )
197250 self .clustering_dbscan_min_samples .bind_visibility_from (
198251 target_object = self .clustering_type ,
@@ -203,14 +256,28 @@ def build_clustering(self):
203256 label = "Clusters" ,
204257 min = 1 ,
205258 value = 2 ,
206- on_change = self .update_handler ,
259+ on_change = self .update_graph_handler ,
207260 ).classes ("w-full" )
208261 self .clustering_agglomerative_n_clusters .bind_visibility_from (
209262 target_object = self .clustering_type ,
210263 target_name = "value" ,
211264 value = CLUSTERING_AGGLOMERATIVE ,
212265 )
213266
267+ def build_draw (self ):
268+ self .draw_3d = ui .toggle (
269+ options = [DRAW_2D , DRAW_3D ],
270+ value = DRAW_3D ,
271+ on_change = self .update_plot_handler ,
272+ )
273+ self .draw_iterations = ui .number (
274+ label = "Layout Iterations" ,
275+ min = 1 ,
276+ max = 1000 ,
277+ value = DRAW_ITERATIONS ,
278+ on_change = self .update_plot_handler ,
279+ )
280+
214281 def build_plot (self ):
215282 fig = go .Figure ()
216283 fig .layout .width = None
@@ -219,6 +286,19 @@ def build_plot(self):
219286 with self .plot_container :
220287 ui .plotly (go .Figure ())
221288
289+ def render_dataset (self ):
290+ source_type = self .data_source_type .value
291+ if source_type == DATA_SOURCE_EXAMPLE :
292+ name = self .data_source_example_file .value
293+ if name == DATA_SOURCE_EXAMPLE_DIGITS :
294+ X , y = load_digits (return_X_y = True , as_frame = True )
295+ return X , y
296+ elif name == DATA_SOURCE_EXAMPLE_IRIS :
297+ X , y = load_iris (return_X_y = True , as_frame = True )
298+ return X , y
299+ elif source_type == DATA_SOURCE_CSV :
300+ pass
301+
222302 def render_lens (self ):
223303 if self .lens_type .value == LENS_IDENTITY :
224304 return _identity
@@ -257,67 +337,94 @@ def render_clustering(self):
257337 n_clusters = int (self .clustering_agglomerative_n_clusters .value )
258338 return AgglomerativeClustering (n_clusters = n_clusters )
259339
260- async def update_handler (self , _ = None ):
261- await run .io_bound (self .update )
340+ async def update_graph_handler (self , _ = None ):
341+ await run .io_bound (self .update_graph )
262342
263- def update (self , _ = None ):
264- X , labels = load_digits ( return_X_y = True )
265- lens = self . render_lens ()
266- if lens is None :
267- return
268- y = lens ( X )
343+ async def update_dataset_handler (self , _ = None ):
344+ await run . io_bound ( self . update_dataset )
345+
346+ def update_dataset ( self , _ = None ) :
347+ self . X , self . labels = self . render_dataset ()
348+ self . update_graph ( )
269349
350+ def update_graph (self , _ = None ):
351+ self .lens = self .render_lens ()
352+ if self .lens is None :
353+ return
354+ if self .X is None :
355+ return
356+ self .y = self .lens (self .X )
270357 cover = self .render_cover ()
271358 if cover is None :
272359 return
273-
274360 clustering = self .render_clustering ()
275361 if clustering is None :
276362 return
277-
278363 mapper_algo = MapperAlgorithm (
279364 cover = cover ,
280365 clustering = clustering ,
281366 verbose = False ,
282367 )
368+ self .mapper_graph = mapper_algo .fit_transform (self .X , self .y )
369+ self .update_plot ()
283370
284- mapper_graph = mapper_algo .fit_transform (X , y )
371+ async def update_plot_handler (self , _ = None ):
372+ await run .io_bound (self .update_plot )
285373
286- mapper_plot = MapperPlot (mapper_graph , dim = 3 , iterations = 400 , seed = 42 )
374+ def update_plot (self ):
375+ if self .mapper_graph is None :
376+ return
287377
378+ dim = 3
379+ if self .draw_3d .value == DRAW_3D :
380+ dim = 3
381+ elif self .draw_3d .value == DRAW_2D :
382+ dim = 2
383+
384+ iterations = int (self .draw_iterations .value )
385+ mapper_plot = MapperPlot (
386+ self .mapper_graph ,
387+ dim = dim ,
388+ iterations = iterations ,
389+ seed = 42 ,
390+ )
288391 mapper_fig = mapper_plot .plot_plotly (
289- colors = labels ,
392+ colors = self . labels ,
290393 cmap = ["jet" , "viridis" , "cividis" ],
291394 agg = mode ,
292395 title = "mode of digits" ,
293396 width = 800 ,
294397 height = 800 ,
295398 node_size = 0.5 ,
296399 )
297- # if mapper_fig.layout.width is not None:
298400 mapper_fig .layout .width = None
299- # if not mapper_fig.layout.autosize:
300401 mapper_fig .layout .autosize = True
301402 self .plot_container .clear ()
302403 with self .plot_container :
303404 ui .plotly (mapper_fig )
304405
305406 def __init__ (self ):
306- with ui .row ().classes ("w-full h-full m-0 p-0 gap-0 overflow-hidden" ):
307- with ui .column ().classes ("w-64 h-full overflow-y-auto m-0 p-3 gap-2" ):
308- with ui .card ().classes ("w-full" ):
309- ui .markdown ("#### 🔎 Lens" )
310- self .build_lens ()
311- with ui .card ().classes ("w-full" ):
312- ui .markdown ("#### 🌐 Cover" )
313- self .build_cover ()
314- with ui .card ().classes ("w-full" ):
315- ui .markdown ("#### 🧮 Clustering" )
316- self .build_clustering ()
407+ with ui .row ().classes ("w-full h-screen m-0 p-0 gap-0 overflow-hidden" ):
408+ with ui .column ().classes ("w-64 h-full m-0 p-0" ): # fixed-width sidebar
409+ with ui .column ().classes ("w-64 h-full overflow-y-auto p-3 gap-2" ):
410+ with ui .card ().classes ("w-full" ):
411+ ui .markdown ("#### 📊 Data" )
412+ self .build_dataset ()
413+ with ui .card ().classes ("w-full" ):
414+ ui .markdown ("#### 🔎 Lens" )
415+ self .build_lens ()
416+ with ui .card ().classes ("w-full" ):
417+ ui .markdown ("#### 🌐 Cover" )
418+ self .build_cover ()
419+ with ui .card ().classes ("w-full" ):
420+ ui .markdown ("#### 🧮 Clustering" )
421+ self .build_clustering ()
317422
318423 with ui .column ().classes ("flex-1 h-full overflow-hidden m-0 p-0" ):
424+ with ui .row (align_items = "baseline" ):
425+ self .build_draw ()
319426 self .build_plot ()
320- self .update ()
427+ self .update_dataset ()
321428
322429
323430app = App ()
0 commit comments