From a5d2674a8d9c1268e90cc2cdbfa8120958bdf6f4 Mon Sep 17 00:00:00 2001 From: Luca Simi Date: Thu, 29 May 2025 21:52:19 +0200 Subject: [PATCH 1/3] First implementation using nicegui --- app/nicegui_app.py | 309 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 app/nicegui_app.py diff --git a/app/nicegui_app.py b/app/nicegui_app.py new file mode 100644 index 0000000..c58ca5e --- /dev/null +++ b/app/nicegui_app.py @@ -0,0 +1,309 @@ +import numpy as np +import plotly.graph_objs as go +from nicegui import ui +from nicegui.events import ValueChangeEventArguments +from sklearn.cluster import AgglomerativeClustering, KMeans +from sklearn.datasets import load_digits, make_circles +from sklearn.decomposition import PCA +from umap import UMAP + +from tdamapper.core import TrivialClustering, TrivialCover +from tdamapper.cover import BallCover, CubicalCover, KNNCover +from tdamapper.learn import MapperAlgorithm +from tdamapper.plot import MapperPlot + + +def mode(arr): + values, counts = np.unique(arr, return_counts=True) + max_count = np.max(counts) + mode_values = values[counts == max_count] + return np.nanmean(mode_values) + + +def _identity(X): + return X + + +def _pca(n_components): + pca = PCA(n_components=n_components, random_state=42) + + def _func(X): + return pca.fit_transform(X) + + return _func + + +def _umap(n_components): + um = UMAP(n_components=n_components, random_state=42) + + def _func(X): + return um.fit_transform(X) + + return _func + + +class App: + + def build_lens(self): + self.opt_lens_id = "Identity" + self.opt_lens_pca = "PCA" + self.opt_lens_umap = "UMAP" + + self.lens_type = ui.select( + label="Lens type", + options=[ + self.opt_lens_id, + self.opt_lens_pca, + self.opt_lens_umap, + ], + value=self.opt_lens_pca, + on_change=self.update, + ).classes("w-full") + self.pca_n_components = ui.number( + label="PCA Components", + min=1, + max=10, + value=2, + on_change=self.update, + ).classes("w-full") + self.pca_n_components.bind_visibility_from( + target_object=self.lens_type, + target_name="value", + value=self.opt_lens_pca, + ) + self.umap_n_components = ui.number( + label="UMAP Components", + min=1, + max=10, + value=2, + on_change=self.update, + ).classes("w-full") + self.umap_n_components.bind_visibility_from( + target_object=self.lens_type, + target_name="value", + value=self.opt_lens_umap, + ) + + def build_cover(self): + self.opt_cover_trivial = "Trivial" + self.opt_cover_cubical = "Cubical" + self.opt_cover_ball = "Ball" + self.opt_cover_knn = "KNN" + + self.cover_type = ui.select( + label="Cover type", + options=[ + self.opt_cover_trivial, + self.opt_cover_cubical, + self.opt_cover_ball, + self.opt_cover_knn, + ], + value=self.opt_cover_cubical, + on_change=self.update, + ).classes("w-full") + self.cover_cubical_n = ui.number( + label="Intervals", + min=1, + max=10, + value=2, + on_change=self.update, + ).classes("w-full") + self.cover_cubical_n.bind_visibility_from( + target_object=self.cover_type, + target_name="value", + value=self.opt_cover_cubical, + ) + self.cover_cubical_overlap = ui.number( + label="Overlap", + min=0.0, + max=1.0, + value=0.5, + on_change=self.update, + ).classes("w-full") + self.cover_cubical_overlap.bind_visibility_from( + target_object=self.cover_type, + target_name="value", + value=self.opt_cover_cubical, + ) + self.cover_ball_radius = ui.number( + label="Radius", + min=0.0, + value=100.0, + on_change=self.update, + ).classes("w-full") + self.cover_ball_radius.bind_visibility_from( + target_object=self.cover_type, + target_name="value", + value=self.opt_cover_ball, + ) + self.cover_knn_k = ui.number( + label="Neighbors", + min=0, + value=10, + on_change=self.update, + ).classes("w-full") + self.cover_knn_k.bind_visibility_from( + target_object=self.cover_type, + target_name="value", + value=self.opt_cover_knn, + ) + + def build_clustering(self): + self.opt_clustering_trivial = "Trivial" + self.opt_clustering_kmeans = "KMeans" + self.opt_clustering_agg = "Agglomerative" + self.opt_clustering_dbscan = "DBSCAN" + + self.clustering_type = ui.select( + label="Clustering type", + options=[ + self.opt_clustering_trivial, + self.opt_clustering_kmeans, + self.opt_clustering_agg, + self.opt_clustering_dbscan, + ], + value=self.opt_clustering_trivial, + on_change=self.update, + ).classes("w-full") + self.clustering_kmeans_k = ui.number( + label="Clusters", + min=1, + value=2, + on_change=self.update, + ).classes("w-full") + self.clustering_kmeans_k.bind_visibility_from( + target_object=self.clustering_type, + target_name="value", + value=self.opt_clustering_kmeans, + ) + self.clustering_dbscan_eps = ui.number( + label="Eps", + min=0.0, + value=0.5, + on_change=self.update, + ).classes("w-full") + self.clustering_dbscan_eps.bind_visibility_from( + target_object=self.clustering_type, + target_name="value", + value=self.opt_clustering_dbscan, + ) + self.clustering_dbscan_min_samples = ui.number( + label="Min Samples", + min=1, + value=5, + on_change=self.update, + ).classes("w-full") + self.clustering_dbscan_eps.bind_visibility_from( + target_object=self.clustering_type, + target_name="value", + value=self.opt_clustering_dbscan, + ) + self.clustering_agg_n = ui.number( + label="Clusters", + min=1, + value=2, + on_change=self.update, + ).classes("w-full") + self.clustering_agg_n.bind_visibility_from( + target_object=self.clustering_type, + target_name="value", + value=self.opt_clustering_agg, + ) + + def build_plot(self): + self.plot = ui.plotly(go.Figure()) + + def render_lens(self): + print(f"Lens type: {self.lens_type.value}") + if self.lens_type.value == self.opt_lens_id: + return _identity + elif self.lens_type.value == self.opt_lens_pca: + n = int(self.pca_n_components.value) + return _pca(n) + elif self.lens_type.value == self.opt_lens_umap: + n = int(self.umap_n_components.value) + return _umap(n) + + def render_cover(self): + if self.cover_type.value == self.opt_cover_trivial: + return TrivialCover() + elif self.cover_type.value == self.opt_cover_ball: + r = float(self.cover_ball_radius.value) + return BallCover(radius=r) + elif self.cover_type.value == self.opt_cover_cubical: + n = int(self.cover_cubical_n.value) + overlap = float(self.cover_cubical_overlap.value) + return CubicalCover(n_intervals=n, overlap_frac=overlap) + elif self.cover_type.value == self.opt_cover_knn: + k = int(self.cover_knn_k.value) + return KNNCover(neighbors=k) + + def render_clustering(self): + if self.clustering_type.value == self.opt_clustering_trivial: + return TrivialClustering() + elif self.clustering_type.value == self.opt_clustering_kmeans: + k = int(self.clustering_kmeans_k.value) + return KMeans(k) + elif self.clustering_type.value == self.opt_clustering_dbscan: + eps = float(self.clustering_dbscan_eps.value) + min_samples = int(self.clustering_dbscan_min_samples.value) + return DBSCAN(eps=eps) + + def update(self, _=None): + X, labels = load_digits(return_X_y=True) + lens = self.render_lens() + if lens is None: + print("Lens is None") + return + y = lens(X) + + cover = self.render_cover() + if cover is None: + print("Cover is None") + return + + clustering = self.render_clustering() + if clustering is None: + print("Clustering is None") + return + + mapper_algo = MapperAlgorithm( + cover=cover, + clustering=clustering, + verbose=False, + ) + + mapper_graph = mapper_algo.fit_transform(X, y) + + mapper_plot = MapperPlot(mapper_graph, dim=3, iterations=400, seed=42) + + mapper_fig = mapper_plot.plot_plotly( + colors=labels, + cmap=["jet", "viridis", "cividis"], + agg=mode, + title="mode of digits", + width=800, + height=800, + node_size=0.5, + ) + if mapper_fig.layout.width is not None: + mapper_fig.layout.width = None + if not mapper_fig.layout.autosize: + mapper_fig.layout.autosize = True + mapper_fig.layout.autosize = True + self.plot.update_figure(mapper_fig) + + def build(self): + with ui.left_drawer().classes("w-[400px]"): + self.build_lens() + ui.separator() + self.build_cover() + ui.separator() + self.build_clustering() + self.build_plot() + self.update() + + +app = App() +app.build() +ui.run() From ee5e8b50a36700a0ee25ee265e9a8280f63ea1e6 Mon Sep 17 00:00:00 2001 From: Luca Simi Date: Fri, 30 May 2025 08:30:00 +0200 Subject: [PATCH 2/3] Added ui elements for cover and clustering. Added async worker function for plot update --- app/nicegui_app.py | 227 ++++++++++++++++++++++++--------------------- 1 file changed, 121 insertions(+), 106 deletions(-) diff --git a/app/nicegui_app.py b/app/nicegui_app.py index c58ca5e..e7a22d3 100644 --- a/app/nicegui_app.py +++ b/app/nicegui_app.py @@ -1,9 +1,8 @@ import numpy as np import plotly.graph_objs as go -from nicegui import ui -from nicegui.events import ValueChangeEventArguments -from sklearn.cluster import AgglomerativeClustering, KMeans -from sklearn.datasets import load_digits, make_circles +from nicegui import run, ui +from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans +from sklearn.datasets import load_digits from sklearn.decomposition import PCA from umap import UMAP @@ -42,229 +41,238 @@ def _func(X): return _func +LENS_IDENTITY = "Identity" +LENS_PCA = "PCA" +LENS_UMAP = "UMAP" + +COVER_TRIVIAL = "Trivial" +COVER_CUBICAL = "Cubical" +COVER_BALL = "Ball" +COVER_KNN = "KNN" + +CLUSTERING_TRIVIAL = "Trivial" +CLUSTERING_KMEANS = "KMeans" +CLUSTERING_AGGLOMERATIVE = "Agglomerative" +CLUSTERING_DBSCAN = "DBSCAN" + + class App: def build_lens(self): - self.opt_lens_id = "Identity" - self.opt_lens_pca = "PCA" - self.opt_lens_umap = "UMAP" - self.lens_type = ui.select( label="Lens type", options=[ - self.opt_lens_id, - self.opt_lens_pca, - self.opt_lens_umap, + LENS_IDENTITY, + LENS_PCA, + LENS_UMAP, ], - value=self.opt_lens_pca, - on_change=self.update, + value=LENS_PCA, + on_change=self.update_handler, ).classes("w-full") self.pca_n_components = ui.number( label="PCA Components", min=1, max=10, value=2, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") self.pca_n_components.bind_visibility_from( target_object=self.lens_type, target_name="value", - value=self.opt_lens_pca, + value=LENS_PCA, ) self.umap_n_components = ui.number( label="UMAP Components", min=1, max=10, value=2, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") self.umap_n_components.bind_visibility_from( target_object=self.lens_type, target_name="value", - value=self.opt_lens_umap, + value=LENS_UMAP, ) def build_cover(self): - self.opt_cover_trivial = "Trivial" - self.opt_cover_cubical = "Cubical" - self.opt_cover_ball = "Ball" - self.opt_cover_knn = "KNN" self.cover_type = ui.select( label="Cover type", options=[ - self.opt_cover_trivial, - self.opt_cover_cubical, - self.opt_cover_ball, - self.opt_cover_knn, + COVER_TRIVIAL, + COVER_CUBICAL, + COVER_BALL, + COVER_KNN, ], - value=self.opt_cover_cubical, - on_change=self.update, + value=COVER_CUBICAL, + on_change=self.update_handler, ).classes("w-full") - self.cover_cubical_n = ui.number( + self.cover_cubical_n_intervals = ui.number( label="Intervals", min=1, - max=10, + max=100, value=2, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") - self.cover_cubical_n.bind_visibility_from( + self.cover_cubical_n_intervals.bind_visibility_from( target_object=self.cover_type, target_name="value", - value=self.opt_cover_cubical, + value=COVER_CUBICAL, ) - self.cover_cubical_overlap = ui.number( + self.cover_cubical_overlap_frac = ui.number( label="Overlap", min=0.0, max=1.0, value=0.5, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") - self.cover_cubical_overlap.bind_visibility_from( + self.cover_cubical_overlap_frac.bind_visibility_from( target_object=self.cover_type, target_name="value", - value=self.opt_cover_cubical, + value=COVER_CUBICAL, ) self.cover_ball_radius = ui.number( label="Radius", min=0.0, value=100.0, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") self.cover_ball_radius.bind_visibility_from( target_object=self.cover_type, target_name="value", - value=self.opt_cover_ball, + value=COVER_BALL, ) - self.cover_knn_k = ui.number( + self.cover_knn_neighbors = ui.number( label="Neighbors", min=0, value=10, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") - self.cover_knn_k.bind_visibility_from( + self.cover_knn_neighbors.bind_visibility_from( target_object=self.cover_type, target_name="value", - value=self.opt_cover_knn, + value=COVER_KNN, ) def build_clustering(self): - self.opt_clustering_trivial = "Trivial" - self.opt_clustering_kmeans = "KMeans" - self.opt_clustering_agg = "Agglomerative" - self.opt_clustering_dbscan = "DBSCAN" - self.clustering_type = ui.select( label="Clustering type", options=[ - self.opt_clustering_trivial, - self.opt_clustering_kmeans, - self.opt_clustering_agg, - self.opt_clustering_dbscan, + CLUSTERING_TRIVIAL, + CLUSTERING_KMEANS, + CLUSTERING_AGGLOMERATIVE, + CLUSTERING_DBSCAN, ], - value=self.opt_clustering_trivial, - on_change=self.update, + value=CLUSTERING_TRIVIAL, + on_change=self.update_handler, ).classes("w-full") - self.clustering_kmeans_k = ui.number( + self.clustering_kmeans_n_clusters = ui.number( label="Clusters", min=1, value=2, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") - self.clustering_kmeans_k.bind_visibility_from( + self.clustering_kmeans_n_clusters.bind_visibility_from( target_object=self.clustering_type, target_name="value", - value=self.opt_clustering_kmeans, + value=CLUSTERING_KMEANS, ) self.clustering_dbscan_eps = ui.number( label="Eps", min=0.0, value=0.5, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") self.clustering_dbscan_eps.bind_visibility_from( target_object=self.clustering_type, target_name="value", - value=self.opt_clustering_dbscan, + value=CLUSTERING_DBSCAN, ) self.clustering_dbscan_min_samples = ui.number( label="Min Samples", min=1, value=5, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") - self.clustering_dbscan_eps.bind_visibility_from( + self.clustering_dbscan_min_samples.bind_visibility_from( target_object=self.clustering_type, target_name="value", - value=self.opt_clustering_dbscan, + value=CLUSTERING_DBSCAN, ) - self.clustering_agg_n = ui.number( + self.clustering_agglomerative_n_clusters = ui.number( label="Clusters", min=1, value=2, - on_change=self.update, + on_change=self.update_handler, ).classes("w-full") - self.clustering_agg_n.bind_visibility_from( + self.clustering_agglomerative_n_clusters.bind_visibility_from( target_object=self.clustering_type, target_name="value", - value=self.opt_clustering_agg, + value=CLUSTERING_AGGLOMERATIVE, ) def build_plot(self): - self.plot = ui.plotly(go.Figure()) + fig = go.Figure() + fig.layout.width = None + fig.layout.autosize = True + self.plot_container = ui.element("div").classes("w-full h-full") + with self.plot_container: + ui.plotly(go.Figure()) def render_lens(self): - print(f"Lens type: {self.lens_type.value}") - if self.lens_type.value == self.opt_lens_id: + if self.lens_type.value == LENS_IDENTITY: return _identity - elif self.lens_type.value == self.opt_lens_pca: + elif self.lens_type.value == LENS_PCA: n = int(self.pca_n_components.value) return _pca(n) - elif self.lens_type.value == self.opt_lens_umap: + elif self.lens_type.value == LENS_UMAP: n = int(self.umap_n_components.value) return _umap(n) def render_cover(self): - if self.cover_type.value == self.opt_cover_trivial: + if self.cover_type.value == COVER_TRIVIAL: return TrivialCover() - elif self.cover_type.value == self.opt_cover_ball: - r = float(self.cover_ball_radius.value) - return BallCover(radius=r) - elif self.cover_type.value == self.opt_cover_cubical: - n = int(self.cover_cubical_n.value) - overlap = float(self.cover_cubical_overlap.value) - return CubicalCover(n_intervals=n, overlap_frac=overlap) - elif self.cover_type.value == self.opt_cover_knn: - k = int(self.cover_knn_k.value) - return KNNCover(neighbors=k) + elif self.cover_type.value == COVER_BALL: + radius = float(self.cover_ball_radius.value) + return BallCover(radius=radius) + elif self.cover_type.value == COVER_CUBICAL: + n_intervals = int(self.cover_cubical_n_intervals.value) + overlap_frac = float(self.cover_cubical_overlap_frac.value) + return CubicalCover(n_intervals=n_intervals, overlap_frac=overlap_frac) + elif self.cover_type.value == COVER_KNN: + neighbors = int(self.cover_knn_neighbors.value) + return KNNCover(neighbors=neighbors) def render_clustering(self): - if self.clustering_type.value == self.opt_clustering_trivial: + if self.clustering_type.value == CLUSTERING_TRIVIAL: return TrivialClustering() - elif self.clustering_type.value == self.opt_clustering_kmeans: - k = int(self.clustering_kmeans_k.value) - return KMeans(k) - elif self.clustering_type.value == self.opt_clustering_dbscan: + elif self.clustering_type.value == CLUSTERING_KMEANS: + n_clusters = int(self.clustering_kmeans_n_clusters.value) + return KMeans(n_clusters) + elif self.clustering_type.value == CLUSTERING_DBSCAN: eps = float(self.clustering_dbscan_eps.value) min_samples = int(self.clustering_dbscan_min_samples.value) - return DBSCAN(eps=eps) + return DBSCAN(eps=eps, min_samples=min_samples) + elif self.clustering_type == CLUSTERING_AGGLOMERATIVE: + n_clusters = int(self.clustering_agglomerative_n_clusters.value) + return AgglomerativeClustering(n_clusters=n_clusters) + + async def update_handler(self, _=None): + await run.io_bound(self.update) def update(self, _=None): X, labels = load_digits(return_X_y=True) lens = self.render_lens() if lens is None: - print("Lens is None") return y = lens(X) cover = self.render_cover() if cover is None: - print("Cover is None") return clustering = self.render_clustering() if clustering is None: - print("Clustering is None") return mapper_algo = MapperAlgorithm( @@ -286,24 +294,31 @@ def update(self, _=None): height=800, node_size=0.5, ) - if mapper_fig.layout.width is not None: - mapper_fig.layout.width = None - if not mapper_fig.layout.autosize: - mapper_fig.layout.autosize = True + # if mapper_fig.layout.width is not None: + mapper_fig.layout.width = None + # if not mapper_fig.layout.autosize: mapper_fig.layout.autosize = True - self.plot.update_figure(mapper_fig) - - def build(self): - with ui.left_drawer().classes("w-[400px]"): - self.build_lens() - ui.separator() - self.build_cover() - ui.separator() - self.build_clustering() - self.build_plot() + self.plot_container.clear() + with self.plot_container: + ui.plotly(mapper_fig) + + def __init__(self): + with ui.row().classes("w-full h-full m-0 p-0 gap-0 overflow-hidden"): + with ui.column().classes("w-64 h-full overflow-y-auto m-0 p-3 gap-2"): + with ui.card().classes("w-full"): + ui.markdown("#### 🔎 Lens") + self.build_lens() + with ui.card().classes("w-full"): + ui.markdown("#### 🌐 Cover") + self.build_cover() + with ui.card().classes("w-full"): + ui.markdown("#### 🧮 Clustering") + self.build_clustering() + + with ui.column().classes("flex-1 h-full overflow-hidden m-0 p-0"): + self.build_plot() self.update() app = App() -app.build() ui.run() From 322de331e55a392fb20ed5abbd77dc4b93c91847 Mon Sep 17 00:00:00 2001 From: Luca Simi Date: Sat, 31 May 2025 08:46:26 +0200 Subject: [PATCH 3/3] Added dataset choice and plot options --- app/nicegui_app.py | 195 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 151 insertions(+), 44 deletions(-) diff --git a/app/nicegui_app.py b/app/nicegui_app.py index e7a22d3..496e215 100644 --- a/app/nicegui_app.py +++ b/app/nicegui_app.py @@ -2,7 +2,7 @@ import plotly.graph_objs as go from nicegui import run, ui from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans -from sklearn.datasets import load_digits +from sklearn.datasets import load_digits, load_iris from sklearn.decomposition import PCA from umap import UMAP @@ -55,9 +55,63 @@ def _func(X): CLUSTERING_AGGLOMERATIVE = "Agglomerative" CLUSTERING_DBSCAN = "DBSCAN" +DATA_SOURCE_EXAMPLE = "Example" +DATA_SOURCE_CSV = "CSV" +DATA_SOURCE_OPENML = "OpenML" + +DATA_SOURCE_EXAMPLE_DIGITS = "Digits" +DATA_SOURCE_EXAMPLE_IRIS = "Iris" + +DRAW_3D = "3D" +DRAW_2D = "2D" +DRAW_ITERATIONS = 50 + class App: + def build_dataset(self): + self.data_source_type = ui.select( + label="Data Source", + options=[ + DATA_SOURCE_EXAMPLE, + DATA_SOURCE_CSV, + DATA_SOURCE_OPENML, + ], + value=DATA_SOURCE_EXAMPLE, + on_change=self.update_dataset_handler, + ).classes("w-full") + self.data_source_example_file = ui.select( + label="File", + options=[ + DATA_SOURCE_EXAMPLE_DIGITS, + DATA_SOURCE_EXAMPLE_IRIS, + ], + value=DATA_SOURCE_EXAMPLE_DIGITS, + on_change=self.update_dataset_handler, + ).classes("w-full") + self.data_source_example_file.bind_visibility_from( + target_object=self.data_source_type, + target_name="value", + value=DATA_SOURCE_EXAMPLE, + ) + self.data_source_csv = ui.upload( + on_upload=self.update_dataset_handler, + ).classes("w-full") + self.data_source_csv.bind_visibility_from( + target_object=self.data_source_type, + target_name="value", + value=DATA_SOURCE_CSV, + ) + self.data_source_openml = ui.input( + label="OpenML Code", + on_change=self.update_dataset_handler, + ).classes("w-full") + self.data_source_openml.bind_visibility_from( + target_object=self.data_source_type, + target_name="value", + value=DATA_SOURCE_OPENML, + ) + def build_lens(self): self.lens_type = ui.select( label="Lens type", @@ -67,14 +121,14 @@ def build_lens(self): LENS_UMAP, ], value=LENS_PCA, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.pca_n_components = ui.number( label="PCA Components", min=1, max=10, value=2, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.pca_n_components.bind_visibility_from( target_object=self.lens_type, @@ -86,7 +140,7 @@ def build_lens(self): min=1, max=10, value=2, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.umap_n_components.bind_visibility_from( target_object=self.lens_type, @@ -95,7 +149,6 @@ def build_lens(self): ) def build_cover(self): - self.cover_type = ui.select( label="Cover type", options=[ @@ -105,14 +158,14 @@ def build_cover(self): COVER_KNN, ], value=COVER_CUBICAL, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.cover_cubical_n_intervals = ui.number( label="Intervals", min=1, max=100, value=2, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.cover_cubical_n_intervals.bind_visibility_from( target_object=self.cover_type, @@ -122,9 +175,9 @@ def build_cover(self): self.cover_cubical_overlap_frac = ui.number( label="Overlap", min=0.0, - max=1.0, - value=0.5, - on_change=self.update_handler, + max=0.5, + value=0.25, + on_change=self.update_graph_handler, ).classes("w-full") self.cover_cubical_overlap_frac.bind_visibility_from( target_object=self.cover_type, @@ -135,7 +188,7 @@ def build_cover(self): label="Radius", min=0.0, value=100.0, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.cover_ball_radius.bind_visibility_from( target_object=self.cover_type, @@ -146,7 +199,7 @@ def build_cover(self): label="Neighbors", min=0, value=10, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.cover_knn_neighbors.bind_visibility_from( target_object=self.cover_type, @@ -164,13 +217,13 @@ def build_clustering(self): CLUSTERING_DBSCAN, ], value=CLUSTERING_TRIVIAL, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.clustering_kmeans_n_clusters = ui.number( label="Clusters", min=1, value=2, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.clustering_kmeans_n_clusters.bind_visibility_from( target_object=self.clustering_type, @@ -181,7 +234,7 @@ def build_clustering(self): label="Eps", min=0.0, value=0.5, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.clustering_dbscan_eps.bind_visibility_from( target_object=self.clustering_type, @@ -192,7 +245,7 @@ def build_clustering(self): label="Min Samples", min=1, value=5, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.clustering_dbscan_min_samples.bind_visibility_from( target_object=self.clustering_type, @@ -203,7 +256,7 @@ def build_clustering(self): label="Clusters", min=1, value=2, - on_change=self.update_handler, + on_change=self.update_graph_handler, ).classes("w-full") self.clustering_agglomerative_n_clusters.bind_visibility_from( target_object=self.clustering_type, @@ -211,6 +264,20 @@ def build_clustering(self): value=CLUSTERING_AGGLOMERATIVE, ) + def build_draw(self): + self.draw_3d = ui.toggle( + options=[DRAW_2D, DRAW_3D], + value=DRAW_3D, + on_change=self.update_plot_handler, + ) + self.draw_iterations = ui.number( + label="Layout Iterations", + min=1, + max=1000, + value=DRAW_ITERATIONS, + on_change=self.update_plot_handler, + ) + def build_plot(self): fig = go.Figure() fig.layout.width = None @@ -219,6 +286,19 @@ def build_plot(self): with self.plot_container: ui.plotly(go.Figure()) + def render_dataset(self): + source_type = self.data_source_type.value + if source_type == DATA_SOURCE_EXAMPLE: + name = self.data_source_example_file.value + if name == DATA_SOURCE_EXAMPLE_DIGITS: + X, y = load_digits(return_X_y=True, as_frame=True) + return X, y + elif name == DATA_SOURCE_EXAMPLE_IRIS: + X, y = load_iris(return_X_y=True, as_frame=True) + return X, y + elif source_type == DATA_SOURCE_CSV: + pass + def render_lens(self): if self.lens_type.value == LENS_IDENTITY: return _identity @@ -257,36 +337,59 @@ def render_clustering(self): n_clusters = int(self.clustering_agglomerative_n_clusters.value) return AgglomerativeClustering(n_clusters=n_clusters) - async def update_handler(self, _=None): - await run.io_bound(self.update) + async def update_graph_handler(self, _=None): + await run.io_bound(self.update_graph) - def update(self, _=None): - X, labels = load_digits(return_X_y=True) - lens = self.render_lens() - if lens is None: - return - y = lens(X) + async def update_dataset_handler(self, _=None): + await run.io_bound(self.update_dataset) + + def update_dataset(self, _=None): + self.X, self.labels = self.render_dataset() + self.update_graph() + def update_graph(self, _=None): + self.lens = self.render_lens() + if self.lens is None: + return + if self.X is None: + return + self.y = self.lens(self.X) cover = self.render_cover() if cover is None: return - clustering = self.render_clustering() if clustering is None: return - mapper_algo = MapperAlgorithm( cover=cover, clustering=clustering, verbose=False, ) + self.mapper_graph = mapper_algo.fit_transform(self.X, self.y) + self.update_plot() - mapper_graph = mapper_algo.fit_transform(X, y) + async def update_plot_handler(self, _=None): + await run.io_bound(self.update_plot) - mapper_plot = MapperPlot(mapper_graph, dim=3, iterations=400, seed=42) + def update_plot(self): + if self.mapper_graph is None: + return + dim = 3 + if self.draw_3d.value == DRAW_3D: + dim = 3 + elif self.draw_3d.value == DRAW_2D: + dim = 2 + + iterations = int(self.draw_iterations.value) + mapper_plot = MapperPlot( + self.mapper_graph, + dim=dim, + iterations=iterations, + seed=42, + ) mapper_fig = mapper_plot.plot_plotly( - colors=labels, + colors=self.labels, cmap=["jet", "viridis", "cividis"], agg=mode, title="mode of digits", @@ -294,30 +397,34 @@ def update(self, _=None): height=800, node_size=0.5, ) - # if mapper_fig.layout.width is not None: mapper_fig.layout.width = None - # if not mapper_fig.layout.autosize: mapper_fig.layout.autosize = True self.plot_container.clear() with self.plot_container: ui.plotly(mapper_fig) def __init__(self): - with ui.row().classes("w-full h-full m-0 p-0 gap-0 overflow-hidden"): - with ui.column().classes("w-64 h-full overflow-y-auto m-0 p-3 gap-2"): - with ui.card().classes("w-full"): - ui.markdown("#### 🔎 Lens") - self.build_lens() - with ui.card().classes("w-full"): - ui.markdown("#### 🌐 Cover") - self.build_cover() - with ui.card().classes("w-full"): - ui.markdown("#### 🧮 Clustering") - self.build_clustering() + with ui.row().classes("w-full h-screen m-0 p-0 gap-0 overflow-hidden"): + with ui.column().classes("w-64 h-full m-0 p-0"): # fixed-width sidebar + with ui.column().classes("w-64 h-full overflow-y-auto p-3 gap-2"): + with ui.card().classes("w-full"): + ui.markdown("#### 📊 Data") + self.build_dataset() + with ui.card().classes("w-full"): + ui.markdown("#### 🔎 Lens") + self.build_lens() + with ui.card().classes("w-full"): + ui.markdown("#### 🌐 Cover") + self.build_cover() + with ui.card().classes("w-full"): + ui.markdown("#### 🧮 Clustering") + self.build_clustering() with ui.column().classes("flex-1 h-full overflow-hidden m-0 p-0"): + with ui.row(align_items="baseline"): + self.build_draw() self.build_plot() - self.update() + self.update_dataset() app = App()