ayyuce commited on
Commit
838838a
·
verified ·
1 Parent(s): 985742b

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +243 -0
  2. requirements.txt +13 -0
  3. templates/index.html +275 -0
app.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import urllib.request
3
+ import tarfile
4
+ import shutil
5
+ from flask import Flask, render_template, request, jsonify, url_for
6
+ from werkzeug.utils import secure_filename
7
+
8
+ import matplotlib
9
+ matplotlib.use('Agg')
10
+
11
+ import scAnalysis.sc_io as io
12
+ import scAnalysis.preprocessing as pp
13
+ import scAnalysis.quality_control as qc
14
+ import scAnalysis.cell_cycle as cc
15
+ import scAnalysis.batch_correction as bc
16
+ import scAnalysis.dimensionality as dim
17
+ import scAnalysis.clustering as cl
18
+ import scAnalysis.trajectory as traj
19
+ import scAnalysis.differential as diff
20
+ import scAnalysis.enrichment as enrich
21
+ import scAnalysis.visualization as vis
22
+ import scAnalysis.interactive_viz as iviz
23
+
24
+ app = Flask(__name__)
25
+ app.config['UPLOAD_FOLDER'] = './static/uploads'
26
+ app.config['RESULTS_FOLDER'] = './static/results'
27
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
28
+ os.makedirs(app.config['RESULTS_FOLDER'], exist_ok=True)
29
+
30
+ DATASETS = {
31
+ "pbmc3k": "https://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz",
32
+ "pbmc5k": "https://cf.10xgenomics.com/samples/cell-exp/3.0.2/5k_pbmc_v3/5k_pbmc_v3_filtered_feature_bc_matrix.tar.gz",
33
+ "heart_atlas": "https://cf.10xgenomics.com/samples/cell-exp/3.0.0/heart_10k_v3/heart_10k_v3_filtered_feature_bc_matrix.tar.gz",
34
+ "mouse_brain": "https://cf.10xgenomics.com/samples/cell-exp/3.0.0/neuron_10k_v3/neuron_10k_v3_filtered_feature_bc_matrix.tar.gz",
35
+ "lung_tumor": "https://cf.10xgenomics.com/samples/cell-exp/3.0.0/nsclc_10k_v3/nsclc_10k_v3_filtered_feature_bc_matrix.tar.gz"
36
+ }
37
+
38
+ def download_and_extract(dataset_name):
39
+ url = DATASETS[dataset_name]
40
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{dataset_name}.tar.gz")
41
+ extract_path = os.path.join(app.config['UPLOAD_FOLDER'], dataset_name)
42
+
43
+ if not os.path.exists(extract_path):
44
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
45
+ with urllib.request.urlopen(req) as response, open(filepath, "wb") as out_file:
46
+ shutil.copyfileobj(response, out_file)
47
+ with tarfile.open(filepath, "r:gz") as tar:
48
+ tar.extractall(path=extract_path)
49
+
50
+ for root, dirs, files in os.walk(extract_path):
51
+ if 'matrix.mtx' in files or 'matrix.mtx.gz' in files:
52
+ return root
53
+ return extract_path
54
+
55
+ @app.route('/')
56
+ def index():
57
+ return render_template('index.html', datasets=DATASETS.keys())
58
+
59
+ @app.route('/run_pipeline', methods=['POST'])
60
+ def run_pipeline():
61
+ try:
62
+ dataset_choice = request.form.get('dataset')
63
+ if dataset_choice == 'custom':
64
+ file = request.files['custom_file']
65
+ filename = secure_filename(file.filename)
66
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
67
+ file.save(file_path)
68
+ data = io.read_h5ad(file_path) if filename.endswith('.h5ad') else io.read_csv(file_path)
69
+ else:
70
+ data_path = download_and_extract(dataset_choice)
71
+ data = io.read_10x_mtx(data_path)
72
+
73
+ data.var.index = io._make_unique(data.var.index.values)
74
+ res_dir = app.config['RESULTS_FOLDER']
75
+ outputs = []
76
+
77
+ pp.calculate_qc_metrics(data, qc_vars=["MT-"])
78
+
79
+ try:
80
+ qc_path = os.path.join(res_dir, 'qc_violin.png')
81
+ vis.plot_qc_violin(data, save=qc_path)
82
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/qc_violin.png'), 'title': 'QC Metrics'})
83
+ except Exception as e: print("QC Plot failed:", e)
84
+
85
+ if request.form.get('run_scrublet') == 'true':
86
+ db_rate = float(request.form.get('doublet_rate', 0.06))
87
+ qc.scrublet(data, expected_doublet_rate=db_rate)
88
+ data = data[~data.obs['predicted_doublet'].astype(bool), :]
89
+
90
+ data = pp.filter_cells(
91
+ data,
92
+ min_genes=int(request.form.get('min_genes', 200)),
93
+ max_genes=int(request.form.get('max_genes', 2500)),
94
+ max_pct_mito=float(request.form.get('max_mito', 5.0))
95
+ )
96
+ data = pp.filter_genes(data, min_cells=int(request.form.get('min_cells', 3)))
97
+
98
+ norm_method = request.form.get('norm_method', 'total')
99
+ if norm_method == 'total':
100
+ pp.normalize_total(data, target_sum=1e4)
101
+ pp.log1p(data)
102
+ elif norm_method == 'scran':
103
+ pp.normalize_scran_pooling(data, target_sum=1e4)
104
+ pp.log1p(data)
105
+ elif norm_method == 'sctransform':
106
+ pp.normalize_sctransform(data)
107
+
108
+ organism = request.form.get('organism', 'human')
109
+ cc.score_cell_cycle(data, organism=organism)
110
+
111
+ n_hvg = int(request.form.get('n_hvg', 2000))
112
+ pp.highly_variable_genes(data, n_top_genes=n_hvg)
113
+
114
+ try:
115
+ hvg_path = os.path.join(res_dir, 'highest_expr_genes.png')
116
+ vis.plot_highest_expr_genes(data, save=hvg_path)
117
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/highest_expr_genes.png'), 'title': 'Highest Expressed'})
118
+ except: pass
119
+
120
+ data.raw = data.copy()
121
+ pp.scale(data, max_value=10)
122
+
123
+ n_pcs = int(request.form.get('n_pcs', 50))
124
+ n_neighbors = int(request.form.get('n_neighbors', 15))
125
+
126
+ dim.run_pca(data, n_components=n_pcs)
127
+ dim.neighbors(data, n_neighbors=n_neighbors, n_pcs=min(40, n_pcs))
128
+
129
+ batch_key = request.form.get('batch_key', '')
130
+ if request.form.get('run_batch') == 'true' and batch_key in data.obs.columns:
131
+ b_algo = request.form.get('batch_algo', 'harmony')
132
+ if b_algo == 'harmony': bc.harmony_integrate(data, batch_key=batch_key)
133
+ elif b_algo == 'combat': bc.combat(data, batch_key=batch_key)
134
+
135
+ if request.form.get('run_umap') == 'true':
136
+ dim.run_umap(data, min_dist=float(request.form.get('umap_min_dist', 0.5)))
137
+ if request.form.get('run_tsne') == 'true':
138
+ dim.run_tsne(data, perplexity=float(request.form.get('tsne_perplex', 30.0)))
139
+ if request.form.get('run_phate') == 'true':
140
+ try: dim.run_phate(data)
141
+ except: pass
142
+
143
+ clust_algo = request.form.get('clustering', 'leiden')
144
+ res_k = float(request.form.get('resolution', 1.0))
145
+
146
+ if clust_algo == 'leiden': cl.cluster_leiden(data, resolution=res_k, key_added="cluster")
147
+ elif clust_algo == 'louvain': cl.cluster_louvain(data, resolution=res_k, key_added="cluster")
148
+ elif clust_algo == 'kmeans': cl.cluster_kmeans(data, n_clusters=int(res_k*10), key_added="cluster")
149
+ elif clust_algo == 'hierarchical': cl.cluster_hierarchical(data, n_clusters=int(res_k*10), key_added="cluster")
150
+ elif clust_algo == 'spectral': cl.cluster_spectral(data, n_clusters=int(res_k*10), key_added="cluster")
151
+
152
+ dim.run_diffmap(data)
153
+ first_cluster = data.obs['cluster'].unique()[0]
154
+ root_strat = request.form.get('root_strategy', 'extreme')
155
+ root_idx = traj.select_root_cell(data, cluster_key='cluster', root_cluster=first_cluster, strategy=root_strat)
156
+ traj.diffusion_pseudotime(data, root_cell=root_idx, n_branchings=int(request.form.get('branches', 0)))
157
+
158
+ diff_method = request.form.get('diff_method', 't-test')
159
+ diff.rank_genes_groups(data, groupby='cluster', method=diff_method, use_raw=True)
160
+
161
+ if 'X_umap' in data.obsm:
162
+ umap_path = os.path.join(res_dir, 'umap_clusters.png')
163
+ vis.plot_umap(data, color="cluster", title="UMAP (Clusters)", save=umap_path)
164
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/umap_clusters.png'), 'title': 'UMAP Clustering'})
165
+
166
+ phase_path = os.path.join(res_dir, 'umap_phase.png')
167
+ vis.plot_umap(data, color="phase", title="Cell Cycle Phase", save=phase_path)
168
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/umap_phase.png'), 'title': 'UMAP Cell Cycle'})
169
+
170
+ if 'X_tsne' in data.obsm:
171
+ tsne_path = os.path.join(res_dir, 'tsne_clusters.png')
172
+ vis.plot_tsne(data, color="cluster", title="t-SNE", save=tsne_path)
173
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/tsne_clusters.png'), 'title': 't-SNE Clustering'})
174
+
175
+ try:
176
+ volcano_path = os.path.join(res_dir, 'volcano_plot.png')
177
+ vis.volcano_plot(data, group=first_cluster, save=volcano_path)
178
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/volcano_plot.png'), 'title': f'Volcano (Cluster {first_cluster})'})
179
+ except: pass
180
+
181
+ canonical_markers = ["CD3D", "CD14", "CD19", "MS4A1", "GNLY", "LYZ", "FCER1A", "CST3", "CD8A"]
182
+ valid_markers = [g for g in canonical_markers if g in data.var.index]
183
+ if valid_markers:
184
+ try:
185
+ dot_path = os.path.join(res_dir, 'dotplot.png')
186
+ vis.plot_dotplot(data, var_names=valid_markers, groupby='cluster', save=dot_path)
187
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/dotplot.png'), 'title': 'Marker Dotplot'})
188
+
189
+ heat_path = os.path.join(res_dir, 'heatmap.png')
190
+ vis.plot_heatmap(data, var_names=valid_markers, groupby='cluster', save=heat_path)
191
+ outputs.append({'type': 'image', 'url': url_for('static', filename='results/heatmap.png'), 'title': 'Marker Heatmap'})
192
+ except: pass
193
+
194
+ hover_data_cols = ['phase', 'total_counts', 'dpt_pseudotime']
195
+
196
+ if request.form.get('int_umap') == 'true' and 'X_umap' in data.obsm:
197
+ try:
198
+ html_umap = os.path.join(res_dir, 'interactive_umap.html')
199
+ iviz.interactive_embedding(data, basis='X_umap', color='cluster', hover_data=hover_data_cols, title="Interactive UMAP", save_html=html_umap)
200
+ outputs.append({'type': 'html', 'url': url_for('static', filename='results/interactive_umap.html'), 'title': 'Interactive UMAP'})
201
+ except Exception as e: print("Interactive UMAP failed:", e)
202
+
203
+ if request.form.get('int_3d_pca') == 'true' and 'X_pca' in data.obsm:
204
+ try:
205
+ html_pca = os.path.join(res_dir, 'interactive_3d_pca.html')
206
+ iviz.interactive_3d_embedding(data, basis='X_pca', color='cluster', dimensions=[0, 1, 2], save_html=html_pca)
207
+ outputs.append({'type': 'html', 'url': url_for('static', filename='results/interactive_3d_pca.html'), 'title': '3D PCA'})
208
+ except Exception as e: print("Interactive PCA failed:", e)
209
+
210
+ if request.form.get('int_tsne') == 'true' and 'X_tsne' in data.obsm:
211
+ try:
212
+ html_tsne = os.path.join(res_dir, 'interactive_tsne.html')
213
+ iviz.interactive_embedding(data, basis='X_tsne', color='cluster', hover_data=hover_data_cols, title="Interactive t-SNE", save_html=html_tsne)
214
+ outputs.append({'type': 'html', 'url': url_for('static', filename='results/interactive_tsne.html'), 'title': 'Interactive t-SNE'})
215
+ except Exception as e: print("Interactive t-SNE failed:", e)
216
+
217
+ if request.form.get('int_violin') == 'true':
218
+ try:
219
+ html_violin = os.path.join(res_dir, 'interactive_violin.html')
220
+ iviz.interactive_violin(data, keys=['n_genes_by_counts', 'total_counts'], groupby='cluster', save_html=html_violin)
221
+ outputs.append({'type': 'html', 'url': url_for('static', filename='results/interactive_violin.html'), 'title': 'Interactive QC Violins'})
222
+ except Exception as e: print("Interactive Violin failed:", e)
223
+
224
+ if request.form.get('int_heatmap') == 'true':
225
+ try:
226
+ if valid_markers:
227
+ html_heat = os.path.join(res_dir, 'interactive_heatmap.html')
228
+ iviz.interactive_heatmap(data, var_names=valid_markers, groupby='cluster', save_html=html_heat)
229
+ outputs.append({'type': 'html', 'url': url_for('static', filename='results/interactive_heatmap.html'), 'title': 'Interactive Heatmap'})
230
+ except Exception as e: print("Interactive Heatmap failed:", e)
231
+
232
+ output_h5ad = os.path.join(res_dir, 'processed_data.h5ad')
233
+ io.write_h5ad(data, output_h5ad)
234
+
235
+ return jsonify({"status": "success", "outputs": outputs, "download": url_for('static', filename='results/processed_data.h5ad')})
236
+
237
+ except Exception as e:
238
+ import traceback
239
+ traceback.print_exc()
240
+ return jsonify({"status": "error", "message": str(e)})
241
+
242
+ if __name__ == '__main__':
243
+ app.run(debug=True, port=5000)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ werkzeug
3
+ plotly
4
+ gunicorn
5
+ numpy
6
+ pandas
7
+ scipy
8
+ scikit-learn
9
+ matplotlib
10
+ seaborn
11
+ h5py
12
+ umap-learn
13
+ scAnalysis
templates/index.html ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>scAnalyzer Studio</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
+ </head>
10
+ <body class="bg-gray-900 text-gray-100 font-sans min-h-screen pb-12">
11
+
12
+ <div class="max-w-7xl mx-auto py-12 px-6">
13
+ <div class="text-center mb-12">
14
+ <h1 class="text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-teal-400 to-indigo-500 mb-4">
15
+ <i class="fas fa-network-wired mr-2 text-teal-400"></i> scAnalysis Studio
16
+ </h1>
17
+ <p class="text-gray-400 text-lg">Full single-cell RNA-seq pipeline.</p>
18
+ </div>
19
+
20
+ <form id="pipelineForm" class="space-y-6">
21
+ <div class="bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700">
22
+ <h2 class="text-xl font-semibold mb-4 text-teal-300"><i class="fas fa-database mr-2"></i> 1. Dataset</h2>
23
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
24
+ {% for ds in datasets %}
25
+ <label class="flex items-center space-x-2 p-3 border border-gray-600 rounded cursor-pointer hover:bg-gray-700 transition">
26
+ <input type="radio" name="dataset" value="{{ ds }}" class="form-radio text-teal-500 h-4 w-4" required>
27
+ <span class="text-gray-200 text-sm capitalize">{{ ds | replace('_', ' ') }}</span>
28
+ </label>
29
+ {% endfor %}
30
+ <label class="flex items-center space-x-2 p-3 border border-teal-600 rounded cursor-pointer bg-teal-900 bg-opacity-20 hover:bg-opacity-40 transition">
31
+ <input type="radio" name="dataset" value="custom" id="customRadio" class="form-radio text-teal-500 h-4 w-4">
32
+ <span class="text-teal-300 text-sm font-bold">Custom Upload</span>
33
+ </label>
34
+ </div>
35
+ <input type="file" name="custom_file" id="customFile" class="hidden w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:bg-teal-600 file:text-white hover:file:bg-teal-700"/>
36
+ </div>
37
+
38
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
39
+
40
+ <div class="bg-gray-800 p-5 rounded-xl shadow-lg border border-gray-700">
41
+ <h2 class="text-lg font-semibold mb-3 text-purple-300"><i class="fas fa-filter mr-2"></i> Quality Control</h2>
42
+ <label class="block text-xs text-gray-400 mb-1">Min Genes & Max Genes</label>
43
+ <div class="flex space-x-2 mb-3">
44
+ <input type="number" name="min_genes" value="200" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm focus:border-purple-500">
45
+ <input type="number" name="max_genes" value="2500" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm focus:border-purple-500">
46
+ </div>
47
+ <label class="block text-xs text-gray-400 mb-1">Max Mito % & Min Cells</label>
48
+ <div class="flex space-x-2 mb-3">
49
+ <input type="number" step="0.1" name="max_mito" value="5.0" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm focus:border-purple-500">
50
+ <input type="number" name="min_cells" value="3" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm focus:border-purple-500">
51
+ </div>
52
+ <label class="flex items-center space-x-2 mt-4">
53
+ <input type="checkbox" name="run_scrublet" value="true" checked class="form-checkbox text-purple-500">
54
+ <span class="text-sm text-gray-300">Run Scrublet</span>
55
+ </label>
56
+ <input type="number" step="0.01" name="doublet_rate" value="0.06" placeholder="Rate (0.06)" class="mt-1 w-full bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm">
57
+ </div>
58
+
59
+ <div class="bg-gray-800 p-5 rounded-xl shadow-lg border border-gray-700">
60
+ <h2 class="text-lg font-semibold mb-3 text-yellow-300"><i class="fas fa-layer-group mr-2"></i> Norm & Batch</h2>
61
+ <label class="block text-xs text-gray-400 mb-1">Normalization</label>
62
+ <select name="norm_method" class="w-full bg-gray-700 border border-gray-600 rounded px-2 py-1 mb-3 text-sm">
63
+ <option value="total">Total Count (Log1p)</option>
64
+ <option value="scran">Scran Pooling</option>
65
+ <option value="sctransform">SCTransform</option>
66
+ </select>
67
+
68
+ <label class="block text-xs text-gray-400 mb-1">Cell Cycle Organism</label>
69
+ <select name="organism" class="w-full bg-gray-700 border border-gray-600 rounded px-2 py-1 mb-3 text-sm">
70
+ <option value="human">Human</option>
71
+ <option value="mouse">Mouse</option>
72
+ </select>
73
+
74
+ <label class="flex items-center space-x-2 mb-1">
75
+ <input type="checkbox" name="run_batch" value="true" class="form-checkbox text-yellow-500">
76
+ <span class="text-sm text-gray-300">Batch Correction</span>
77
+ </label>
78
+ <div class="flex space-x-2">
79
+ <select name="batch_algo" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs">
80
+ <option value="harmony">Harmony</option>
81
+ <option value="combat">ComBat</option>
82
+ </select>
83
+ <input type="text" name="batch_key" placeholder="Col Name" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs">
84
+ </div>
85
+ </div>
86
+
87
+ <div class="bg-gray-800 p-5 rounded-xl shadow-lg border border-gray-700">
88
+ <h2 class="text-lg font-semibold mb-3 text-blue-300"><i class="fas fa-project-diagram mr-2"></i> Dimensionality</h2>
89
+
90
+ <div class="grid grid-cols-2 gap-2 mb-3">
91
+ <div>
92
+ <label class="block text-xs text-gray-400 mb-1">PCA Comps</label>
93
+ <input type="number" name="n_pcs" value="50" class="w-full bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm">
94
+ </div>
95
+ <div>
96
+ <label class="block text-xs text-gray-400 mb-1">Neighbors (K)</label>
97
+ <input type="number" name="n_neighbors" value="15" class="w-full bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm">
98
+ </div>
99
+ </div>
100
+
101
+ <div class="space-y-2 mt-4">
102
+ <label class="flex items-center justify-between">
103
+ <div class="flex items-center space-x-2">
104
+ <input type="checkbox" name="run_umap" value="true" checked class="form-checkbox text-blue-500">
105
+ <span class="text-sm text-gray-300">UMAP</span>
106
+ </div>
107
+ <input type="number" step="0.1" name="umap_min_dist" value="0.5" class="w-16 bg-gray-700 border border-gray-600 rounded px-1 py-0.5 text-xs text-center" title="Min Dist">
108
+ </label>
109
+ <label class="flex items-center justify-between">
110
+ <div class="flex items-center space-x-2">
111
+ <input type="checkbox" name="run_tsne" value="true" class="form-checkbox text-blue-500">
112
+ <span class="text-sm text-gray-300">t-SNE</span>
113
+ </div>
114
+ <input type="number" step="0.1" name="tsne_perplex" value="30" class="w-16 bg-gray-700 border border-gray-600 rounded px-1 py-0.5 text-xs text-center" title="Perplexity">
115
+ </label>
116
+ <label class="flex items-center space-x-2">
117
+ <input type="checkbox" name="run_phate" value="true" class="form-checkbox text-blue-500">
118
+ <span class="text-sm text-gray-300">PHATE</span>
119
+ </label>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="bg-gray-800 p-5 rounded-xl shadow-lg border border-gray-700">
124
+ <h2 class="text-lg font-semibold mb-3 text-pink-300"><i class="fas fa-chart-pie mr-2"></i> Analytics</h2>
125
+
126
+ <label class="block text-xs text-gray-400 mb-1">Clustering & Res/K</label>
127
+ <div class="flex space-x-2 mb-3">
128
+ <select name="clustering" class="w-2/3 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs">
129
+ <option value="leiden">Leiden</option>
130
+ <option value="louvain">Louvain</option>
131
+ <option value="kmeans">K-Means</option>
132
+ <option value="hierarchical">Hierarchical</option>
133
+ <option value="spectral">Spectral</option>
134
+ </select>
135
+ <input type="number" step="0.1" name="resolution" value="1.0" class="w-1/3 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm text-center">
136
+ </div>
137
+
138
+ <label class="block text-xs text-gray-400 mb-1">Differential Method</label>
139
+ <select name="diff_method" class="w-full bg-gray-700 border border-gray-600 rounded px-2 py-1 mb-3 text-xs">
140
+ <option value="t-test">T-Test</option>
141
+ <option value="wilcoxon">Wilcoxon Rank-Sum</option>
142
+ </select>
143
+
144
+ <label class="block text-xs text-gray-400 mb-1">Trajectory (Root & Branches)</label>
145
+ <div class="flex space-x-2 mb-3">
146
+ <select name="root_strategy" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs">
147
+ <option value="extreme">Extreme</option>
148
+ <option value="medoid">Medoid</option>
149
+ </select>
150
+ <input type="number" name="branches" value="0" placeholder="Branches" class="w-1/2 bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs">
151
+ </div>
152
+
153
+ <hr class="border-gray-700 my-3">
154
+ <label class="block mb-2 text-sm text-gray-300 font-bold">Interactive HTML Plots:</label>
155
+ <div class="space-y-1">
156
+ <label class="flex items-center space-x-2">
157
+ <input type="checkbox" name="int_umap" value="true" checked class="form-checkbox text-pink-500 h-4 w-4">
158
+ <span class="text-xs text-gray-300">Interactive UMAP</span>
159
+ </label>
160
+ <label class="flex items-center space-x-2">
161
+ <input type="checkbox" name="int_3d_pca" value="true" checked class="form-checkbox text-pink-500 h-4 w-4">
162
+ <span class="text-xs text-gray-300">3D Interactive PCA</span>
163
+ </label>
164
+ <label class="flex items-center space-x-2">
165
+ <input type="checkbox" name="int_tsne" value="true" class="form-checkbox text-pink-500 h-4 w-4">
166
+ <span class="text-xs text-gray-300">Interactive t-SNE</span>
167
+ </label>
168
+ <label class="flex items-center space-x-2">
169
+ <input type="checkbox" name="int_violin" value="true" checked class="form-checkbox text-pink-500 h-4 w-4">
170
+ <span class="text-xs text-gray-300">QC Violins</span>
171
+ </label>
172
+ <label class="flex items-center space-x-2">
173
+ <input type="checkbox" name="int_heatmap" value="true" checked class="form-checkbox text-pink-500 h-4 w-4">
174
+ <span class="text-xs text-gray-300">Marker Heatmap</span>
175
+ </label>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <button type="submit" class="w-full bg-gradient-to-r from-teal-600 to-indigo-600 hover:from-teal-500 hover:to-indigo-500 text-white font-bold py-4 rounded-xl shadow-lg transform transition hover:scale-[1.01] text-xl">
181
+ <i class="fas fa-play mr-2"></i> Execute Analysis
182
+ </button>
183
+ </form>
184
+
185
+ <div id="statusArea" class="hidden mt-10 bg-gray-800 p-8 rounded-xl shadow-lg border border-gray-700">
186
+ <div id="loader" class="text-center py-8">
187
+ <i class="fas fa-circle-notch fa-spin text-5xl text-teal-500 mb-4"></i>
188
+ <p class="text-xl text-gray-200 font-semibold blink">Processing single-cell pipeline...</p>
189
+ <p class="text-gray-400 mt-2 text-sm">Keep an eye on your terminal. Large datasets will take time.</p>
190
+ </div>
191
+
192
+ <div id="results" class="hidden">
193
+ <div class="flex justify-between items-center mb-6 border-b border-gray-700 pb-4">
194
+ <h2 class="text-2xl font-bold text-green-400"><i class="fas fa-check-circle mr-2"></i> Success</h2>
195
+ <a id="downloadBtn" href="#" class="bg-green-600 hover:bg-green-500 text-white font-bold py-2 px-4 rounded shadow">
196
+ <i class="fas fa-download mr-2"></i> Download .h5ad
197
+ </a>
198
+ </div>
199
+
200
+ <div id="interactiveContainer" class="flex flex-wrap gap-3 mb-6"></div>
201
+ <div id="plotContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <script>
207
+ document.getElementById('customRadio').addEventListener('change', function() {
208
+ document.getElementById('customFile').classList.remove('hidden');
209
+ });
210
+ document.querySelectorAll('input[name="dataset"]:not(#customRadio)').forEach(el => {
211
+ el.addEventListener('change', () => document.getElementById('customFile').classList.add('hidden'));
212
+ });
213
+
214
+ document.getElementById('pipelineForm').addEventListener('submit', async (e) => {
215
+ e.preventDefault();
216
+
217
+ document.getElementById('statusArea').classList.remove('hidden');
218
+ document.getElementById('loader').classList.remove('hidden');
219
+ document.getElementById('results').classList.add('hidden');
220
+ document.getElementById('plotContainer').innerHTML = '';
221
+ document.getElementById('interactiveContainer').innerHTML = '';
222
+
223
+ const formData = new FormData(e.target);
224
+
225
+ try {
226
+ const response = await fetch('/run_pipeline', { method: 'POST', body: formData });
227
+
228
+ if (!response.ok) {
229
+ const errText = await response.text();
230
+ document.getElementById('loader').classList.add('hidden');
231
+ alert("SERVER ERROR 500: Check the terminal running Python. Something crashed inside scAnalysis.");
232
+ return;
233
+ }
234
+
235
+ const data = await response.json();
236
+ document.getElementById('loader').classList.add('hidden');
237
+
238
+ if (data.status === 'success') {
239
+ document.getElementById('results').classList.remove('hidden');
240
+ document.getElementById('downloadBtn').href = data.download;
241
+
242
+ data.outputs.forEach(out => {
243
+ if (out.type === 'image') {
244
+ document.getElementById('plotContainer').innerHTML += `
245
+ <div class="bg-gray-900 rounded p-3 border border-gray-700 flex flex-col items-center">
246
+ <h3 class="text-gray-300 text-sm font-semibold mb-2">${out.title}</h3>
247
+ <a href="${out.url}" target="_blank">
248
+ <img src="${out.url}?t=${new Date().getTime()}" class="rounded hover:opacity-80 transition object-contain max-h-56">
249
+ </a>
250
+ </div>
251
+ `;
252
+ } else if (out.type === 'html') {
253
+ document.getElementById('interactiveContainer').innerHTML += `
254
+ <a href="${out.url}" target="_blank" class="flex-1 bg-gray-700 hover:bg-gray-600 text-teal-300 text-sm font-medium py-2 px-3 rounded shadow border border-gray-600 text-center transition">
255
+ <i class="fas fa-mouse-pointer mr-2"></i> ${out.title}
256
+ </a>
257
+ `;
258
+ }
259
+ });
260
+ } else {
261
+ alert("Pipeline Error: " + data.message);
262
+ }
263
+ } catch (err) {
264
+ document.getElementById('loader').classList.add('hidden');
265
+ alert("Request completely failed. Ensure the Flask server is still running.");
266
+ console.error(err);
267
+ }
268
+ });
269
+ </script>
270
+ <style>
271
+ .blink { animation: blinker 1.5s linear infinite; }
272
+ @keyframes blinker { 50% { opacity: 0.5; } }
273
+ </style>
274
+ </body>
275
+ </html>