Skip to content

Commit 322de33

Browse files
committed
Added dataset choice and plot options
1 parent ee5e8b5 commit 322de33

1 file changed

Lines changed: 151 additions & 44 deletions

File tree

app/nicegui_app.py

Lines changed: 151 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import plotly.graph_objs as go
33
from nicegui import run, ui
44
from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans
5-
from sklearn.datasets import load_digits
5+
from sklearn.datasets import load_digits, load_iris
66
from sklearn.decomposition import PCA
77
from umap import UMAP
88

@@ -55,9 +55,63 @@ def _func(X):
5555
CLUSTERING_AGGLOMERATIVE = "Agglomerative"
5656
CLUSTERING_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

5970
class 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

323430
app = App()

0 commit comments

Comments
 (0)