diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01a1c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules + +datasets/**/*.npz +*.hdf5 + +yarn-error.log +.DS_STORE + +.pyc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ca3eb98 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "bracketSpacing": true, + "printWidth": 120, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c0b8414 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.words": [ + "ncells", + "numpy", + "scipy", + "sklearn" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 837d51c..d9fd2b4 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,35 @@ -### Installing Python libraries - -To run SPRING Viewer locally, make sure Python 2.7 is installed (and that it's your active version). You will also need the following Python libraries: - -`scikit-learn` -`numpy` -`scipy` -`matplotlib` -`h5py` -`networkx` -`fa2` -`python-louvain` +# SPRING -We recommend Anaconda to manage your Python libraries. You can download it here (be sure to get the Python 2.7 version): https://conda.io/miniconda.html. Libraries can then be installed using the command `conda`. To do so, open Terminal (Mac) or Anaconda Prompt (Windows) and enter: + -`conda install scikit-learn numpy scipy matplotlib h5py` +- [SPRING](#spring) + - [Setting Up A SPRING Data Directory](#setting-up-a-spring-data-directory) + - [Backend](#backend) + - [Installing Python libraries](#installing-python-libraries) + - [Frontend](#frontend) + - [Development Build](#development-build) + - [Production Build](#production-build) + - [RequireJS](#requirejs) + - [Running SPRING Viewer](#running-spring-viewer) -The remaining libraries can be installed using `pip`. Note that if you're a Windows user, you'll first need to install Microsoft Visual C++ compiler for Python (available from http://aka.ms/vcpython27). Enter the following into Terminal or Anaconda Prompt: + -`pip install networkx fa2 python-louvain` +[SPRING](https://doi.org/10.1093/bioinformatics/btx792) is a collection of pre-processing scripts and a web browser-based tool for visualizing and interacting with high dimensional data. +## Setting Up A SPRING Data Directory -### Setting up a SPRING data directory See the example notebooks: [Hematopoietic progenitor FACS subpopulations](./data_prep/spring_example_HPCs.ipynb) -[Mature blood cells (10X Genomics 4k PBMCs)](./data_prep/spring_example_pbmc4k.ipynb) +[Mature blood cells (10X Genomics 4k PBMCs)](./data_prep/spring_example_pbmc4k.ipynb) -A SPRING data set consist of a main directory and any number of subdirectories, with each subdirectory corresponding to one SPRING plot (i.e. subplot) that draws on a data matrix stored in the main directory. The main directory should have the following files, as well as one subdirectory for each SPRING plot. +A SPRING data set consist of a main directory and any number of subdirectories, with each subdirectory corresponding to one SPRING plot (i.e. subplot) that draws on a data matrix stored in the main directory. The main directory should have the following files, as well as one subdirectory for each SPRING plot. `counts_norm.npz` `counts_norm_sparse_cells.hdf5` `counts_norm_sparse_genes.hdf5` -`genes.txt` +`genes.txt` -Each subdirectory should contain: +Each subdirectory should contain: `categorical_coloring_data.json` `cell_filter.npy` @@ -42,15 +39,65 @@ Each subdirectory should contain: `coordinates.txt` `edges.csv` `graph_data.json` -`run_info.json` +`run_info.json` + +Place the main directory somewhere inside folder that contains this README and the other SPRING file. We recommend that you create a special `datasets` directory. For example, if you have a main data set called `human_bone_marrow` and another called `frog_embryo`, you could place them in `./datasets/human_bone_marrow/` and `./datasets/frog_embryo/`. + +## Backend + +### Installing Python libraries + +To run SPRING Viewer locally, make sure Python 2.7 is installed (and that it's your active version). You will also need the following Python libraries: + +`scikit-learn` +`numpy` +`scipy` +`h5py` +`networkx` +`fa2` +`python-louvain` + +We recommend Anaconda to manage your Python libraries. You can download it here (be sure to get the Python 2.7 version): https://conda.io/miniconda.html. Libraries can then be installed using the command `conda`. To do so, open Terminal (Mac) or Anaconda Prompt (Windows) and enter: + +`conda install scikit-learn numpy scipy h5py` + +The remaining libraries can be installed using `pip`. Note that if you're a Windows user, you'll first need to install Microsoft Visual C++ compiler for Python (available from http://aka.ms/vcpython27). Enter the following into Terminal or Anaconda Prompt: + +`pip install networkx fa2 python-louvain` + +## Frontend -Place the main directory somehwere inside folder that contains this README and the other SPRING file. We recommend that you create a special `datasets` directory. For example, if you have a main data set called `human_bone_marrow` and another called `frog_embryo`, you could place them in `./datasets/human_bone_marrow/` and `./datasets/frog_embryo/`. +The SPRING frontend is setup as a JavaScript module using [Yarn](https://yarnpkg.com/en/) as a package manager and [TypeScript](https://www.typescriptlang.org/) as a transpiler - Meaning we can write code that uses features like [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) while still supporting older browsers! +### Development Build -### Running SPRING Viewer +To get the frontend building for development purpose, open your favorite terminal/shell into this directory and then run: -1. Open Terminal (Mac) or Anaconda Prompt (Windows) and change directories (`cd`) to the directory containing this README file (`SPRING_dev/`). +```sh +yarn +yarn build --watch +``` + +This will compile code inside `src/` and put it inside `dist/` as well as watch for file changes and re-compile as needed. Neat! + +### Production Build + +Similar to the above, run the following two commands to create the production build: + +```sh +yarn +yarn build +``` + +### RequireJS + +Further, please note there is only a single entry point for the app on the springViewer.html page - namely, `app.js`. Digging down a bit, you'll see that it is setup as a [RequireJS](https://requirejs.org/) module and is **pulling code from the vendor and dist directories**! + +This setup seems to provide the most flexibility in allowing spring to come bundled with minified 3rd party library code (d3, jquery, pixi.js, etc), while still allowing the app code to be setup in a modular way and not adding in a lot of extra configuration using tools like Rollup or Webpack. + +## Running SPRING Viewer + +1. Open Terminal (Mac) or Anaconda Prompt (Windows) and change directories (`cd`) to the directory containing this README file (`SPRING_dev/`). 2. Start a local server by entering the following: `python -m CGIHTTPServer 8000` 3. Open web browser (preferably Chrome; best to use incognito mode to ensure no cached data is used). -4. View data set by navigating to corresponding URL: http://localhost:8000/springViewer_1_6_dev.html?path_to/main/subplot. In the example above, if you wanted to view a SPRING plot called `HSC` in the main directory `human_bone_marrow`, then you would navigate to http://localhost:8000/springViewer_1_6_dev.html?datasets/human_bone_marrow/HSC - +4. View data set by navigating to corresponding URL: http://localhost:8000/springViewer.html?path_to/main/subplot. In the example above, if you wanted to view a SPRING plot called `HSC` in the main directory `human_bone_marrow`, then you would navigate to http://localhost:8000/springViewer.html?datasets/human_bone_marrow/HSC diff --git a/app.js b/app.js new file mode 100644 index 0000000..9268da6 --- /dev/null +++ b/app.js @@ -0,0 +1,57 @@ +requirejs.config({ + baseUrl: 'vendor', + shim: { + PIXI: { exports: 'PIXI' }, + }, + paths: { + dist: '../dist', + d3: 'd3.v5.min', + html2canvas: 'html2canvas.min', + spinner: 'spin.min', + sweetalert: 'sweetalert.min', + }, +}); + +if (!window.cacheData) { + window.cacheData = new Map(); +} + +window.addEventListener('message', event => { + if (!event.isTrusted && event.origin === window.location.origin) { + return; + } + try { + if (typeof event.data === 'string') { + const parsedData = JSON.parse(event.data); + switch (parsedData.type) { + case 'init': { + if (parsedData.payload.indices) { + window.cacheData.set('indices', parsedData.payload.indices); + } + + if (parsedData.payload.categories) { + window.cacheData.set('categories', parsedData.payload.categories); + } + } + case 'selected-cells-update': { + if (parsedData.payload.coordinates) { + window.cacheData.set('selected-cells', parsedData.payload.coordinates); + } + } + default: { + break; + } + } + } + } catch (err) { + console.log(`Unable to parse received message.\n\ + Data: ${event.data} + Error: ${err}`); + } +}); + +requirejs.config({ + waitSeconds: 200, +}); + +requirejs(['dist/main']); diff --git a/cgi-bin/apply_gene_set_retrospective.py b/cgi-bin/apply_gene_set_retrospective.py index 71f6a5e..208846c 100644 --- a/cgi-bin/apply_gene_set_retrospective.py +++ b/cgi-bin/apply_gene_set_retrospective.py @@ -1,6 +1,9 @@ #!/usr/bin/env python #========================================================================================# -import numpy as np, sys, h5py, json +import numpy as np +import sys +import h5py +import json base_dir = sys.argv[1] sub_dirs = sys.argv[2] @@ -9,58 +12,58 @@ hf = h5py.File(base_dir + '/counts_norm_sparse_genes.hdf5', 'r') ncells = hf.attrs['ncells'] valid_genes = hf.get('counts').keys() -gene_map = {g.split()[0]:g for g in valid_genes} +gene_map = {g.split()[0]: g for g in valid_genes} # Load gene sets gene_sets = {} all_genes = set([]) -for l in open(gene_sets_path).read().replace('\r','\n').split('\n'): - l = l.split('\t') - if len(l) > 1: - gene = l[0] - name = l[1] - if gene in gene_map: gene = gene_map[gene] - if not gene in valid_genes: print 'Invalid',gene - else: - if not name in gene_sets: - gene_sets[name] = [] - gene_sets[name].append(gene) - all_genes.add(gene) +for l in open(gene_sets_path).read().replace('\r', '\n').split('\n'): + split_l = l.split('\t') + if len(l) > 1: + gene = split_l[0] + name = split_l[1] + if gene in gene_map: + gene = gene_map[gene] + if gene not in valid_genes: + print('Invalid', gene) + else: + if name not in gene_sets: + gene_sets[name] = [] + gene_sets[name].append(gene) + all_genes.add(gene) -# Load gene expression +# Load gene expression gene_exp = {} for g in all_genes: - ee = np.zeros(ncells) - counts = np.array(hf.get('counts').get(g)) - cell_ix = np.array(hf.get('cell_ix').get(g)) - ee[cell_ix] = counts - gene_exp[g] = ee + ee = np.zeros(ncells) + counts = np.array(hf.get('counts').get(g)) + cell_ix = np.array(hf.get('cell_ix').get(g)) + ee[cell_ix] = counts + gene_exp[g] = ee # compute scores scores = {} -for k,gs in gene_sets.items(): - Z = np.array([gene_exp[g] for g in gs]) - Z = (Z - np.mean(Z,axis=1)[:,None]) / (np.std(Z,axis=1)[:,None] + .0001) - ss = np.sum(Z,axis=0) - ss = ss - np.min(ss) - scores[k] = ss +for k, gs in gene_sets.items(): + Z = np.array([gene_exp[g] for g in gs]) + Z = (Z - np.mean(Z, axis=1)[:, None]) / \ + (np.std(Z, axis=1)[:, None] + .0001) + ss = np.sum(Z, axis=0) + ss = ss - np.min(ss) + scores[k] = ss # Apply to each subplots for dd in sub_dirs.split(','): - cell_ix = np.load(base_dir+'/'+dd+'/cell_filter.npy') - f = open(base_dir+'/'+dd+'/color_data_gene_sets.csv','a') - for k,ss in scores.items(): - newline = ','.join([k]+[repr(x) for x in ss[cell_ix]]) - f.write(newline+'\n') - f.close() - - color_stats = json.load(open(base_dir+'/'+dd+'/color_stats.json')) - for k,ss in scores.items(): - color_stats[k] = (np.mean(ss),np.std(ss),np.min(ss),np.max(ss),np.percentile(ss,99)) - json.dump(color_stats,open(base_dir+'/'+dd+'/color_stats.json','w')) - - - + cell_ix = np.load(base_dir + '/' + dd + '/cell_filter.npy') + f = open(base_dir + '/' + dd + '/color_data_gene_sets.csv', 'a') + for k, ss in scores.items(): + newline = ','.join([k] + [repr(x) for x in ss[cell_ix]]) + f.write(newline + '\n') + f.close() + color_stats = json.load(open(base_dir + '/' + dd + '/color_stats.json')) + for k, ss in scores.items(): + color_stats[k] = (np.mean(ss), np.std(ss), np.min(ss), + np.max(ss), np.percentile(ss, 99)) + json.dump(color_stats, open(base_dir + '/' + dd + '/color_stats.json', 'w')) diff --git a/cgi-bin/cluster_retrofit.py b/cgi-bin/cluster_retrofit.py index 576c002..387014f 100755 --- a/cgi-bin/cluster_retrofit.py +++ b/cgi-bin/cluster_retrofit.py @@ -1,102 +1,119 @@ #!/usr/bin/env python +import json +import matplotlib.pyplot as plt +import numpy as np +import os +import sklearn +import sys +import time + from scipy.sparse.linalg import eigs from sklearn.metrics import silhouette_score #========================================================================================# + + def spec_clust(A, k): - spec = sklearn.cluster.SpectralClustering(n_clusters=k, affinity = 'precomputed',assign_labels='discretize') + spec = sklearn.cluster.SpectralClustering( + n_clusters=k, affinity='precomputed', assign_labels='discretize') return spec.fit_predict(A) + def load_coords(DIRECTORY): - xcoords = {} - ycoords = {} - for l in open(DIRECTORY + '/coordinates.txt').read().split('\n')[:-1]: - l = l.split(',') - cell_number = int(l[0]) - xcoords[cell_number] = float(l[1]) - ycoords[cell_number] = float(l[2]) - xx,yy = [],[] - for k in sorted(xcoords.keys()): - xx += [xcoords[k]] - yy += [ycoords[k]] - xx = np.array(xx) - yy = np.array(yy) - return xx,-yy - -def spectral_coords(A,k): - X,Y = np.meshgrid(np.sum(A,axis=0),np.sum(A,axis=0)) - A = A / np.sqrt(X * Y) - L = np.identity(A.shape[0]) - A - w,v = eigs(L,k=k, which='SR') - return w,v + xcoords = {} + ycoords = {} + for l in open(DIRECTORY + '/coordinates.txt').read().split('\n')[:-1]: + split_l = l.split(',') + cell_number = int(split_l[0]) + xcoords[cell_number] = float(split_l[1]) + ycoords[cell_number] = float(split_l[2]) + xx, yy = [], [] + for k in sorted(xcoords.keys()): + xx += [xcoords[k]] + yy += [ycoords[k]] + xx = np.array(xx) + yy = np.array(yy) + return xx, -yy + + +def spectral_coords(A, k): + X, Y = np.meshgrid(np.sum(A, axis=0), np.sum(A, axis=0)) + A = A / np.sqrt(X * Y) + L = np.identity(A.shape[0]) - A + w, v = eigs(L, k=k, which='SR') + return w, v + def row_norm_normalize(X): - total_counts = np.sqrt(np.sum(X**2,axis=1)) - tc_tiled = np.tile(total_counts[:,None],(1,X.shape[1])) - return X / tc_tiled + total_counts = np.sqrt(np.sum(X**2, axis=1)) + tc_tiled = np.tile(total_counts[:, None], (1, X.shape[1])) + return X / tc_tiled + def frac_to_hex(frac): - rgb = tuple(np.array(np.array(plt.cm.jet(frac)[:3])*255,dtype=int)) - return '#%02x%02x%02x' % rgb + rgb = tuple(np.array(np.array(plt.cm.jet(frac)[:3]) * 255, dtype=int)) + return '#%02x%02x%02x' % rgb #========================================================================================# + t = time.time() -project_directory = 'client_datasets/'+ sys.argv[1] -print 'start',project_directory +project_directory = 'client_datasets/' + sys.argv[1] +print('start', project_directory) clustering = {} graph_path = project_directory + '/graph_data.json' -print graph_path +print(graph_path) if os.path.exists(graph_path): - print 'doing' - graph = json.load(open(graph_path)) - node_numbers = [n['number'] for n in graph['nodes']] - node_index = {nn:i for i,nn in enumerate(node_numbers)} - A = np.zeros((len(node_numbers),len(node_numbers))) - for l in graph['links']: - i = node_index[l['source']] - j = node_index[l['target']] - A[i,j] = 1 - A[j,i] = 1 - print project_directory,'Spectral start' - ww,vv = spectral_coords(A,A.shape[0]/2) - ww = np.real(ww) - spectral_gaps = list((np.roll(ww,-1)-ww))[:-1] - maxk = np.argmax(spectral_gaps)*2 + 2 - maxk = np.max([maxk,20]) - spectral_gaps = spectral_gaps[:maxk] - - first_peak = 0 - has_risen = False - falling = False - while (not (has_risen and falling)) and first_peak < len(spectral_gaps): - first_peak += 1 - has_risen = has_risen or (spectral_gaps[first_peak+1] - spectral_gaps[first_peak] > 0) - falling = spectral_gaps[first_peak+1] - spectral_gaps[first_peak] < 0 - first_peak += 1 - if has_risen == False: first_peak = 1 - print 'FP',first_peak - - - X = row_norm_normalize(vv) - for k in range(1,np.min([A.shape[0]/4,100])): - print project_directory,'Cluster',k - km = sklearn.cluster.KMeans(n_clusters=k) - clus = km.fit_predict(X[:,:k]) - clustering['Cluster'+repr(k)] = [repr(x+1) for x in clus] - - clus_colored = {} - for k,labels in clustering.items(): - label_colors = {l:frac_to_hex(float(i)/len(set(labels))) for i,l in enumerate(list(set(labels)))} - clus_colored[k] = {'label_colors':label_colors, 'label_list':labels} - data = {'Current_clustering':'Cluster'+repr(first_peak), - 'clusters':clus_colored, - 'spectral_info':{'gaps':spectral_gaps, 'argmax':first_peak}} - json.dump(data,open('clustering_data/' + sys.argv[1] + '_clustering_data.json','w'),indent=4) - -print 'TOTAL TIME',time.time() - t + print('doing') + graph = json.load(open(graph_path)) + node_numbers = [n['number'] for n in graph['nodes']] + node_index = {nn: i for i, nn in enumerate(node_numbers)} + A = np.zeros((len(node_numbers), len(node_numbers))) + for l in graph['links']: + i = node_index[l['source']] + j = node_index[l['target']] + A[i, j] = 1 + A[j, i] = 1 + print(project_directory, 'Spectral start') + ww, vv = spectral_coords(A, A.shape[0] / 2) + ww = np.real(ww) + spectral_gaps = list((np.roll(ww, -1) - ww))[:-1] + maxk = np.argmax(spectral_gaps) * 2 + 2 + maxk = np.max([maxk, 20]) + spectral_gaps = spectral_gaps[:maxk] + + first_peak = 0 + has_risen = False + falling = False + while (not (has_risen and falling)) and first_peak < len(spectral_gaps): + first_peak += 1 + has_risen = has_risen or ( + spectral_gaps[first_peak + 1] - spectral_gaps[first_peak] > 0) + falling = spectral_gaps[first_peak + 1] - spectral_gaps[first_peak] < 0 + first_peak += 1 + if has_risen is False: + first_peak = 1 + print('FP', first_peak) + + X = row_norm_normalize(vv) + for k in range(1, np.min([A.shape[0] / 4, 100])): + print(project_directory, 'Cluster', k) + km = sklearn.cluster.KMeans(n_clusters=k) + clus = km.fit_predict(X[:, :k]) + clustering['Cluster' + repr(k)] = [repr(x + 1) for x in clus] + + clus_colored = {} + for k, labels in clustering.items(): + label_colors = {l: frac_to_hex(float(i) / len(set(labels))) + for i, l in enumerate(list(set(labels)))} + clus_colored[k] = {'label_colors': label_colors, 'label_list': labels} + data = {'Current_clustering': 'Cluster' + repr(first_peak), + 'clusters': clus_colored, + 'spectral_info': {'gaps': spectral_gaps, 'argmax': first_peak}} + json.dump(data, open('clustering_data/' + sys.argv[1] + '_clustering_data.json', 'w'), indent=4) + +print('TOTAL TIME', time.time() - t) # xx,yy = load_coords('.') # plt.scatter(xx,yy,c=clus,edgecolor='') # plt.show() - diff --git a/cgi-bin/delete_subdirectory.py b/cgi-bin/delete_subdirectory.py index 6dd26fc..92d070a 100755 --- a/cgi-bin/delete_subdirectory.py +++ b/cgi-bin/delete_subdirectory.py @@ -9,8 +9,7 @@ cgitb.enable() # for troubleshooting -print "Content-Type: text/html" -print +print('Content-Type: text/html') form = cgi.FieldStorage() @@ -19,12 +18,8 @@ proj_dir = form.getvalue('base_dir').strip('\n') sub_dir = form.getvalue('sub_dir').strip('\n') -if not os.path.exists(proj_dir+'/archive'): - os.system('mkdir '+proj_dir+'/archive') +if not os.path.exists(proj_dir + '/archive'): + os.system('mkdir '+proj_dir + '/archive') from shutil import move move(proj_dir + '/' + sub_dir, proj_dir + '/archive/' + sub_dir) - - - - diff --git a/cgi-bin/doublet_helper.py b/cgi-bin/doublet_helper.py index 561b8a0..03aa7ff 100755 --- a/cgi-bin/doublet_helper.py +++ b/cgi-bin/doublet_helper.py @@ -703,7 +703,7 @@ def make_spring_subplot(E, gene_list, save_path, base_ix = None, normalize = Tru gene_filter = np.arange(E.shape[1]) if len(gene_filter) == 0: - print 'Error: No genes passed filter' + print('Error: No genes passed filter') sys.exit(2) #print 'Error: All genes have mean expression < '+repr(min_exp) + ' or CV < '+repr(min_cv) #print 'Using %i genes' %(len(gene_filter)) @@ -711,7 +711,7 @@ def make_spring_subplot(E, gene_list, save_path, base_ix = None, normalize = Tru if not exclude_corr_genes_list is None: gene_filter = remove_corr_genes(E, gene_list, exclude_corr_genes_list, gene_filter, min_corr = exclude_corr_genes_minCorr) if len(gene_filter) == 0: - print 'Error: No genes passed filter' + print ('Error: No genes passed filter') sys.exit(2) # Remove user-excluded genes from consideration @@ -720,7 +720,7 @@ def make_spring_subplot(E, gene_list, save_path, base_ix = None, normalize = Tru #print 'Excluded %i user-provided genes' %(len(gene_filter)-len(keep_ix)) gene_filter = gene_filter[keep_ix] if len(gene_filter) == 0: - print 'Error: No genes passed filter' + print ('Error: No genes passed filter') sys.exit(2) out['gene_filter'] = gene_filter @@ -922,8 +922,8 @@ def plot_groups(x, y, groups, lim_buffer = 50, saving = False, fig_dir = './', f def rank_enriched_genes(E, gene_list, cell_mask, min_counts=3, min_cells=3): gix = (E[cell_mask,:]>=min_counts).sum(0).A.squeeze() >= min_cells - print '%i cells in group' %(sum(cell_mask)) - print 'Considering %i genes' %(sum(gix)) + print ('%i cells in group' %(sum(cell_mask))) + print ('Considering %i genes' %(sum(gix))) gene_list = gene_list[gix] diff --git a/cgi-bin/download_expression.execute.py b/cgi-bin/download_expression.execute.py index 8d37634..3895eec 100755 --- a/cgi-bin/download_expression.execute.py +++ b/cgi-bin/download_expression.execute.py @@ -20,27 +20,31 @@ t00 = time.time() + def sparse_var(E, axis=0): mean_gene = E.mean(axis=axis).A.squeeze() tmp = E.copy() tmp.data **= 2 return tmp.mean(axis=axis).A.squeeze() - mean_gene ** 2 + def update_log_html(fname, logdat, overwrite=False): - if overwrite: - o = open(fname, 'w') - else: - o = open(fname, 'a') - o.write(logdat + '
\n') - o.close() + if overwrite: + o = open(fname, 'w') + else: + o = open(fname, 'a') + o.write(logdat + '
\n') + o.close() + def update_log(fname, logdat, overwrite=False): - if overwrite: - o = open(fname, 'w') - else: - o = open(fname, 'a') - o.write(logdat + '\n') - o.close() + if overwrite: + o = open(fname, 'w') + else: + o = open(fname, 'a') + o.write(logdat + '\n') + o.close() + def send_confirmation_email(user_email, subset_name, path): @@ -53,7 +57,7 @@ def send_confirmation_email(user_email, subset_name, path): msg = MIMEMultipart() msg['From'] = fromaddr msg['To'] = toaddr - msg['Subject'] = 'Your download "%s" is ready' %subset_name + msg['Subject'] = 'Your download "%s" is ready' % subset_name body = 'Download data here:\n' + path + '\n' msg.attach(MIMEText(body, 'plain')) @@ -65,6 +69,7 @@ def send_confirmation_email(user_email, subset_name, path): server.sendmail(fromaddr, toaddr, text) server.quit() + ####################### # Load parameters params_dict = pickle.load(open(sys.argv[1], 'rb')) @@ -78,9 +83,9 @@ def send_confirmation_email(user_email, subset_name, path): my_url_origin = params_dict['my_url_origin'] -#outdir = current_dir + '/selected_downloads/tmp_download_' + rand_suffix + '/' +# outdir = current_dir + '/selected_downloads/tmp_download_' + rand_suffix + '/' outdir = "downloads/" + selection_name + '.' + rand_suffix + '/' -#os.makedirs(outdir) +# os.makedirs(outdir) # logf = outdir + 'logdownloadselect.txt' # timef = outdir + 'logdownloadselecttime.txt' @@ -89,35 +94,36 @@ def send_confirmation_email(user_email, subset_name, path): # Load data cell_filter = np.load(current_dir + '/cell_filter.npy')[extra_filter] -gene_list = np.loadtxt(base_dir + '/genes.txt', dtype=str, delimiter='\t', comments="") +gene_list = np.loadtxt(base_dir + '/genes.txt', + dtype=str, delimiter='\t', comments="") t0 = time.time() # update_log_html(logf, 'Loading counts data...', True) E = ssp.load_npz(base_dir + '/counts_norm.npz') -E = E[cell_filter,:] +E = E[cell_filter, :] if not ssp.isspmatrix_csc(E): E = E.tocsc() t1 = time.time() # update_log(timef, 'Counts loaded from npz -- %.2f' %(t1-t0), True) -print E.shape -print gene_list.shape +print(E.shape) +print(gene_list.shape) E = E.T -print E.shape +print(E.shape) ################# # Save expression matrix as csv -print 'Saving expression' +print('Saving expression') o = open(outdir + 'expr.csv', 'w') t0 = time.time() for iG, g in enumerate(gene_list): if iG % 500 == 0: t1 = time.time() - #update_log(timef, 'Gene %i -- %.2f' %(iG + 1, t1-t0)) + # update_log(timef, 'Gene %i -- %.2f' %(iG + 1, t1-t0)) t0 = time.time() - counts = E[iG,:].A.squeeze() + counts = E[iG, :].A.squeeze() o.write(g + ',' + ','.join(map(str, counts)) + '\n') o.close() @@ -125,17 +131,19 @@ def send_confirmation_email(user_email, subset_name, path): # save coordinates -print 'Saving coordinates' -coords = np.loadtxt(current_dir + '/coordinates.txt', delimiter = ',', comments="")[:,1:] -coords = coords[extra_filter,:] -np.savetxt(outdir + 'coordinates.csv', np.hstack((np.arange(coords.shape[0])[:,None], coords)), fmt="%i,%.5f,%.5f") +print('Saving coordinates') +coords = np.loadtxt(current_dir + '/coordinates.txt', + delimiter=',', comments="")[:, 1:] +coords = coords[extra_filter, :] +np.savetxt(outdir + 'coordinates.csv', + np.hstack((np.arange(coords.shape[0])[:, None], coords)), fmt="%i,%.5f,%.5f") # save original cell indices of selected cells -print 'Saving cell indices' +print('Saving cell indices') np.savetxt(outdir + 'original_cell_indices.txt', cell_filter, fmt='%i') # save extra categorical variables -print 'Saving categorical data' +print('Saving categorical data') categ = json.load(open(current_dir + '/categorical_coloring_data.json')) o = open(outdir + 'cell_groupings.csv', 'w') for k in categ: @@ -145,7 +153,7 @@ def send_confirmation_email(user_email, subset_name, path): o.close() # save extra continuous variables -print 'Saving continuous data' +print('Saving continuous data') o = open(outdir + 'custom_colors.csv', 'w') with open(current_dir + '/color_data_gene_sets.csv') as f: for l in f: @@ -160,6 +168,8 @@ def send_confirmation_email(user_email, subset_name, path): #################### outdir_strip = outdir.strip('/') -os.system('cd downloads; tar cfz "' + selection_name + '.' + rand_suffix + '.tar.gz" "' + selection_name + '.' + rand_suffix + '"') +os.system('cd downloads; tar cfz "' + selection_name + '.' + + rand_suffix + '.tar.gz" "' + selection_name + '.' + rand_suffix + '"') -send_confirmation_email(user_email, selection_name, my_url_origin + '/' + outdir_strip + '.tar.gz') +send_confirmation_email(user_email, selection_name, + my_url_origin + '/' + outdir_strip + '.tar.gz') diff --git a/cgi-bin/download_expression.submit.py b/cgi-bin/download_expression.submit.py index 9aa8bbd..f9b0179 100755 --- a/cgi-bin/download_expression.submit.py +++ b/cgi-bin/download_expression.submit.py @@ -2,8 +2,7 @@ import cgi import cgitb cgitb.enable() # for troubleshooting -print "Content-Type: text/html" -print +print("Content-Type: text/html\n") import os import pickle @@ -29,45 +28,46 @@ all_errors = [] if selection_name is None: - all_errors.append('Enter a cell subset name
') - do_the_rest = False + all_errors.append('Enter a cell subset name
') + do_the_rest = False if extra_filter is None: - all_errors.append('No cells selected.
') - do_the_rest = False + all_errors.append('No cells selected.
') + do_the_rest = False bad_chars = [" ", "/", "\\", ",", ":", "#", "\"", "\'"] found_bad = [] if not selection_name is None: - for b in bad_chars: - if b in selection_name: - do_the_rest = False - if b == " ": - found_bad.append('space') - else: - found_bad.append(b) + for b in bad_chars: + if b in selection_name: + do_the_rest = False + if b == " ": + found_bad.append('space') + else: + found_bad.append(b) if len(found_bad) > 0: - all_errors.append('Enter a cell subset name without the following characters: %s
' %(' '.join(found_bad))) + all_errors.append( + 'Enter a cell subset name without the following characters: %s
' % (' '.join(found_bad))) try: - user_email = data.getvalue('email') - if "@" not in user_email: - all_errors.append('Enter a valid email address.
') - do_the_rest = False + user_email = data.getvalue('email') + if "@" not in user_email: + all_errors.append( + 'Enter a valid email address.
') + do_the_rest = False except: - user_email = '' + user_email = '' if not do_the_rest: - #os.rmdir(new_dir) - print 'Invalid input!
' - for err in all_errors: - print '> %s' %err + # os.rmdir(new_dir) + print('Invalid input!
') + for err in all_errors: + print('> %s' % err) else: try: rand_suffix = ''.join(str(time.time()).split('.')) - extra_filter = np.sort(np.array(map(int,extra_filter.split(',')))) - + extra_filter = np.sort(np.array(map(int, extra_filter.split(',')))) params_dict = {} params_dict['extra_filter'] = extra_filter @@ -79,17 +79,19 @@ params_dict['my_url_origin'] = my_url_origin out_dir = 'downloads/' - params_filename = "downloads/" + selection_name + "." + rand_suffix + "/download_params.pickle" + params_filename = "downloads/" + selection_name + \ + "." + rand_suffix + "/download_params.pickle" os.makedirs("downloads/" + selection_name + "." + rand_suffix) params_file = open(params_filename, 'wb') pickle.dump(params_dict, params_file, -1) params_file.close() - print 'Preparing data...
' - print 'This may take several minutes.
' - print 'You will be notified of completion by email.
' - print '
Feel free to close this window.
' + print('Preparing data...
') + print('This may take several minutes.
') + print('You will be notified of completion by email.
') + print('
Feel free to close this window.
') - subprocess.call(["cgi-bin/download_expression.submit.sh", params_filename]) + subprocess.call( + ["cgi-bin/download_expression.submit.sh", params_filename]) except: - print 'Error starting processing!
' + print('Error starting processing!
') diff --git a/cgi-bin/get_gene_zscores.from_hdf5.dev.py b/cgi-bin/get_gene_zscores.from_hdf5.dev.py index 592a512..70fee0a 100755 --- a/cgi-bin/get_gene_zscores.from_hdf5.dev.py +++ b/cgi-bin/get_gene_zscores.from_hdf5.dev.py @@ -13,16 +13,19 @@ if cwd.endswith('cgi-bin'): os.chdir('../') + def update_log(fname, logdat, overwrite=False): - if overwrite: - o = open(fname, 'w') - else: - o = open(fname, 'a') - o.write(logdat + '\n') - o.close() + if overwrite: + o = open(fname, 'w') + else: + o = open(fname, 'a') + o.write(logdat + '\n') + o.close() + def strfloat(x): - return "%.3f" %x + return "%.3f" % x + cgitb.enable() # for troubleshooting data = cgi.FieldStorage() @@ -35,26 +38,28 @@ def strfloat(x): logf = 'tmplogenrich' update_log(logf, 'Enrichment log:', True) -gene_list = np.loadtxt(base_dir + '/genes.txt', dtype=str, delimiter='\t', comments="") +gene_list = np.loadtxt(base_dir + '/genes.txt', + dtype=str, delimiter='\t', comments="") if str(sel_filter) != "None": - sel_filter = np.sort(np.array(map(int,sel_filter.split(',')))) + sel_filter = np.sort(np.array(map(int, sel_filter.split(',')))) else: sel_filter = [] sel_scores = np.zeros(len(gene_list), dtype=float) if str(comp_filter) != "None": - comp_filter = np.sort(np.array(map(int,comp_filter.split(',')))) + comp_filter = np.sort(np.array(map(int, comp_filter.split(',')))) else: comp_filter = [] comp_scores = np.zeros(len(gene_list), dtype=float) -update_log(logf, '%i selected cells; %i compared cells' %(len(sel_filter), len(comp_filter)), False) +update_log(logf, '%i selected cells; %i compared cells' % + (len(sel_filter), len(comp_filter)), False) t0 = time.time() color_stats = json.load(open(sub_dir + '/color_stats.json', 'r')) t1 = time.time() -update_log(logf, 'got color stats -- %.3f' %(t1-t0)) +update_log(logf, 'got color stats -- %.3f' % (t1-t0)) hf = h5py.File(base_dir + '/counts_norm_sparse_cells.hdf5', 'r') hf_counts = hf.get('counts') @@ -62,10 +67,11 @@ def strfloat(x): if len(sel_filter) > 0: t0 = time.time() - cell_filter = np.array(np.load(sub_dir + '/cell_filter.npy')[sel_filter], dtype=str) + cell_filter = np.array( + np.load(sub_dir + '/cell_filter.npy')[sel_filter], dtype=str) totals = np.zeros(len(gene_list), dtype=float) t1 = time.time() - update_log(logf, 'got cell filter -- %.3f' %(t1-t0)) + update_log(logf, 'got cell filter -- %.3f' % (t1-t0)) t0 = time.time() for cellid in cell_filter: @@ -75,7 +81,7 @@ def strfloat(x): all_means = totals / float(len(cell_filter)) t1 = time.time() - update_log(logf, 'got means -- %.3f' %(t1-t0)) + update_log(logf, 'got means -- %.3f' % (t1-t0)) t0 = time.time() sel_scores = [] @@ -85,11 +91,12 @@ def strfloat(x): sel_scores.append((all_means[iG] - m) / (s+0.02)) sel_scores = np.array(sel_scores) t1 = time.time() - update_log(logf, 'got selected scores -- %.3f' %(t1-t0)) + update_log(logf, 'got selected scores -- %.3f' % (t1-t0)) if len(comp_filter) > 0: t0 = time.time() - cell_filter = np.array(np.load(sub_dir + '/cell_filter.npy')[comp_filter], dtype=str) + cell_filter = np.array( + np.load(sub_dir + '/cell_filter.npy')[comp_filter], dtype=str) totals = np.zeros(len(gene_list), dtype=float) for cellid in cell_filter: @@ -99,7 +106,7 @@ def strfloat(x): all_means = totals / float(len(cell_filter)) t1 = time.time() - update_log(logf, 'got means -- %.3f' %(t1-t0)) + update_log(logf, 'got means -- %.3f' % (t1-t0)) t0 = time.time() comp_scores = [] @@ -110,22 +117,21 @@ def strfloat(x): comp_scores = np.array(comp_scores) t1 = time.time() - update_log(logf, 'got compared scores -- %.3f' %(t1-t0)) + update_log(logf, 'got compared scores -- %.3f' % (t1-t0)) scores = sel_scores - comp_scores t0 = time.time() o = np.argsort(-scores)[:1000] t1 = time.time() -update_log(logf, 'sorted scores -- %.3f' %(t1-t0)) +update_log(logf, 'sorted scores -- %.3f' % (t1-t0)) t0 = time.time() gene_list = gene_list[o] scores = scores[o] t1 = time.time() -update_log(logf, 'filtered lists -- %.3f' %(t1-t0)) +update_log(logf, 'filtered lists -- %.3f' % (t1-t0)) hf.close() -print "Content-Type: text/plain" -print -print '\n'.join(gene_list) + '\t' + '\n'.join(map(strfloat,scores)) +print("Content-Type: text/plain\n") +print('\n'.join(gene_list) + '\t' + '\n'.join(map(strfloat, scores))) diff --git a/cgi-bin/get_gene_zscores.from_npz.dev.py b/cgi-bin/get_gene_zscores.from_npz.dev.py index 7116385..26b0a6d 100755 --- a/cgi-bin/get_gene_zscores.from_npz.dev.py +++ b/cgi-bin/get_gene_zscores.from_npz.dev.py @@ -11,16 +11,19 @@ if cwd.endswith('cgi-bin'): os.chdir('../') + def update_log(fname, logdat, overwrite=False): - if overwrite: - o = open(fname, 'w') - else: - o = open(fname, 'a') - o.write(logdat + '\n') - o.close() + if overwrite: + o = open(fname, 'w') + else: + o = open(fname, 'a') + o.write(logdat + '\n') + o.close() + def strfloat(x): - return "%.3f" %x + return "%.3f" % x + cgitb.enable() # for troubleshooting data = cgi.FieldStorage() @@ -33,44 +36,46 @@ def strfloat(x): logf = 'tmplogenrich' update_log(logf, 'Enrichment log:', True) -gene_list = np.loadtxt(base_dir + '/genes.txt', dtype=str, delimiter='\t', comments="") +gene_list = np.loadtxt(base_dir + '/genes.txt', + dtype=str, delimiter='\t', comments="") if str(sel_filter) != "None": - sel_filter = np.sort(np.array(map(int,sel_filter.split(',')))) + sel_filter = np.sort(np.array(map(int, sel_filter.split(',')))) else: sel_filter = [] sel_scores = np.zeros(len(gene_list), dtype=float) if str(comp_filter) != "None": - comp_filter = np.sort(np.array(map(int,comp_filter.split(',')))) + comp_filter = np.sort(np.array(map(int, comp_filter.split(',')))) else: comp_filter = [] comp_scores = np.zeros(len(gene_list), dtype=float) -update_log(logf, '%i selected cells; %i compared cells' %(len(sel_filter), len(comp_filter)), False) +update_log(logf, '%i selected cells; %i compared cells' % + (len(sel_filter), len(comp_filter)), False) t0 = time.time() color_stats = json.load(open(sub_dir + '/color_stats.json', 'r')) t1 = time.time() -update_log(logf, 'got color stats -- %.3f' %(t1-t0)) +update_log(logf, 'got color stats -- %.3f' % (t1-t0)) t0 = time.time() E = ssp.load_npz(base_dir + '/counts_norm.npz') t1 = time.time() -update_log(logf, 'loaded npz -- %.3f' %(t1-t0)) +update_log(logf, 'loaded npz -- %.3f' % (t1-t0)) t0 = time.time() cell_filter = np.load(sub_dir + '/cell_filter.npy') t1 = time.time() -update_log(logf, 'got cell filter -- %.3f' %(t1-t0)) +update_log(logf, 'got cell filter -- %.3f' % (t1-t0)) if len(sel_filter) > 0: cell_filter_use = cell_filter[sel_filter] t0 = time.time() - all_means = E[cell_filter_use,:].mean(0).A.squeeze() + all_means = E[cell_filter_use, :].mean(0).A.squeeze() t1 = time.time() - update_log(logf, 'got means -- %.3f' %(t1-t0)) + update_log(logf, 'got means -- %.3f' % (t1-t0)) t0 = time.time() sel_scores = np.zeros(len(gene_list), dtype=float) @@ -79,15 +84,15 @@ def strfloat(x): s = color_stats[g][1] sel_scores[iG] = (all_means[iG] - m) / (s+0.02) t1 = time.time() - update_log(logf, 'got selected scores -- %.3f' %(t1-t0)) + update_log(logf, 'got selected scores -- %.3f' % (t1-t0)) if len(comp_filter) > 0: cell_filter_use = cell_filter[comp_filter] t0 = time.time() - all_means = E[cell_filter_use,:].mean(0).A.squeeze() + all_means = E[cell_filter_use, :].mean(0).A.squeeze() t1 = time.time() - update_log(logf, 'got means -- %.3f' %(t1-t0)) + update_log(logf, 'got means -- %.3f' % (t1-t0)) t0 = time.time() comp_scores = np.zeros(len(gene_list), dtype=float) @@ -96,19 +101,18 @@ def strfloat(x): s = color_stats[g][1] comp_scores[iG] = (all_means[iG] - m) / (s+0.02) t1 = time.time() - update_log(logf, 'got compared scores -- %.3f' %(t1-t0)) + update_log(logf, 'got compared scores -- %.3f' % (t1-t0)) scores = sel_scores - comp_scores t0 = time.time() o = np.argsort(-scores)[:1000] t1 = time.time() -update_log(logf, 'sorted scores -- %.3f' %(t1-t0)) +update_log(logf, 'sorted scores -- %.3f' % (t1-t0)) t0 = time.time() gene_list = gene_list[o] scores = scores[o] t1 = time.time() -update_log(logf, 'filtered lists -- %.3f' %(t1-t0)) -print "Content-Type: text/plain" -print -print '\n'.join(gene_list) + '\t' + '\n'.join(map(strfloat,scores)) +update_log(logf, 'filtered lists -- %.3f' % (t1-t0)) +print("Content-Type: text/plain\n") +print('\n'.join(gene_list) + '\t' + '\n'.join(map(strfloat, scores))) diff --git a/cgi-bin/grab_one_gene.py b/cgi-bin/grab_one_gene.py index 99a61c5..5d05103 100755 --- a/cgi-bin/grab_one_gene.py +++ b/cgi-bin/grab_one_gene.py @@ -1,64 +1,73 @@ #!/usr/bin/env python +import h5py +import numpy as np +import cgi import time import os cwd = os.getcwd() if cwd.endswith('cgi-bin'): os.chdir('../') - + t00 = time.time() + + def update_log(fname, logdat, overwrite=False): - if overwrite: - o = open(fname, 'w') - else: - o = open(fname, 'a') - o.write(logdat + '\n') - o.close() + if overwrite: + o = open(fname, 'w') + else: + o = open(fname, 'a') + o.write(logdat + '\n') + o.close() + + def strfloat(x): - if x == 0: return "0" - else: return "%.1f" %x + if x == 0: + return '0' + else: + return '%.1f' % x + + logf = 'tmplog2' t0 = time.time() -import cgi t1 = time.time() -update_log(logf, 'import cgi -- %.3f' %(t1-t0), True) +update_log(logf, 'import cgi -- %.3f' % (t1 - t0), True) t0 = time.time() -import numpy as np t1 = time.time() -update_log(logf, 'import numpy -- %.3f' %(t1-t0)) +update_log(logf, 'import numpy -- %.3f' % (t1 - t0)) t0 = time.time() -import h5py t1 = time.time() -update_log(logf, 'import h5py -- %.3f' %(t1-t0)) +update_log(logf, 'import h5py -- %.3f' % (t1 - t0)) t0 = time.time() data = cgi.FieldStorage() base_dir = data.getvalue('base_dir') sub_dir = data.getvalue('sub_dir') gene = data.getvalue('gene') + t1 = time.time() -update_log(logf, 'got cgi data -- %.3f' %(t1-t0)) +update_log(logf, 'got cgi data -- %.3f' % (t1 - t0)) update_log(logf, gene) t0 = time.time() -#cell_filter = np.array(map(int, data.getvalue('cell_filter').split(','))) -cell_filter = np.load(sub_dir + '/' + 'cell_filter.npy') +# cell_filter = np.array(map(int, data.getvalue('cell_filter').split(','))) +cell_filter = np.load(sub_dir + '/' + 'cell_filter.npy') t1 = time.time() -update_log(logf, 'got cell filter-- %.3f' %(t1-t0)) +update_log(logf, 'got cell filter-- %.3f' % (t1 - t0)) -#t0 = time.time() -#cell_filter = np.loadtxt(sub_dir + '/cell_filter.txt', dtype=int) -##full_cell_filter = np.loadtxt(base_dir + '/cell_filter.txt', dtype=int) -#t1 = time.time() -#update_log(logf, 'loaded cell filters -- %.3f' %(t1-t0)) -#update_log(logf, str(len(cell_filter))) -#update_log(logf, str(len(cf))) -#update_log(logf, str(np.all(cf == cell_filter))) +# t0 = time.time() +# cell_filter = np.loadtxt(sub_dir + '/cell_filter.txt', dtype=int) +# full_cell_filter = np.loadtxt(base_dir + '/cell_filter.txt', dtype=int) +# t1 = time.time() +# update_log(logf, 'loaded cell filters -- %.3f' %(t1-t0)) +# update_log(logf, str(len(cell_filter))) +# update_log(logf, str(len(cf))) +# update_log(logf, str(np.all(cf == cell_filter))) t0 = time.time() hf = h5py.File(base_dir + '/counts_norm_sparse_genes.hdf5', 'r') @@ -67,30 +76,29 @@ def strfloat(x): ncells = hf.attrs['ncells'] hf.close() t1 = time.time() -update_log(logf, 'loaded hdf5 data -- %.3f' %(t1-t0)) +update_log(logf, 'loaded hdf5 data -- %.3f' % (t1 - t0)) t0 = time.time() -#E = np.zeros(len(full_cell_filter), dtype=float) +# E = np.zeros(len(full_cell_filter), dtype=float) E = np.zeros(ncells, dtype=float) t1 = time.time() -update_log(logf, 'inialized array -- %.3f' %(t1-t0)) +update_log(logf, 'inialized array -- %.3f' % (t1 - t0)) t0 = time.time() E[cell_ix] = counts t1 = time.time() -update_log(logf, 'filled array -- %.3f' %(t1-t0)) +update_log(logf, 'filled array -- %.3f' % (t1 - t0)) t0 = time.time() E = E[cell_filter] t1 = time.time() -update_log(logf, 'filtered array -- %.3f' %(t1-t0)) +update_log(logf, 'filtered array -- %.3f' % (t1 - t0)) t0 = time.time() -print "Content-Type: text/plain" -print -print '\n'.join(map(strfloat,E)) +print('Content-Type: text/plain\n') +print('\n'.join(map(strfloat, E))) t1 = time.time() -update_log(logf, 'returned data -- %.3f' %(t1-t0)) +update_log(logf, 'returned data -- %.3f' % (t1 - t0)) t11 = time.time() -update_log(logf, str(t11-t00)) +update_log(logf, str(t11 - t00)) diff --git a/cgi-bin/list_directories_with_filename.py b/cgi-bin/list_directories_with_filename.py index 139942f..189c850 100755 --- a/cgi-bin/list_directories_with_filename.py +++ b/cgi-bin/list_directories_with_filename.py @@ -9,8 +9,7 @@ cgitb.enable() # for troubleshooting -print "Content-Type: text/html" -print +print("Content-Type: text/html\n") data = cgi.FieldStorage() @@ -18,6 +17,6 @@ filename = data.getvalue('filename') out = [] for f in os.listdir(path): - if os.path.exists(path+'/'+f+'/'+filename): - out.append(f) -print ','.join(sorted(out,key=lambda x: x.lower())) + if os.path.exists(path+'/'+f+'/'+filename): + out.append(f) +print(','.join(sorted(out, key=lambda x: x.lower()))) diff --git a/cgi-bin/load_counts.py b/cgi-bin/load_counts.py index cf979ab..c4ca29f 100755 --- a/cgi-bin/load_counts.py +++ b/cgi-bin/load_counts.py @@ -12,7 +12,7 @@ base_dir = data.getvalue('base_dir') gene_list = [l.strip('\n') for l in open(base_dir + '/genes.txt')] -print "Content-Type: text/plain" -print +print('Content-Type: text/plain\n') + for g in gene_list: - print g + print(g) diff --git a/cgi-bin/run_clustering.py b/cgi-bin/run_clustering.py index 923ecb0..0292e88 100755 --- a/cgi-bin/run_clustering.py +++ b/cgi-bin/run_clustering.py @@ -5,10 +5,11 @@ import os import json cgitb.enable() # for troubleshooting -print "Content-Type: text/plain\n" +print("Content-Type: text/plain\n") #========================================================================================# + def update_log(fname, logdat, overwrite=False): if overwrite: o = open(fname, 'w') @@ -17,44 +18,48 @@ def update_log(fname, logdat, overwrite=False): o.write(logdat + '\n') o.close() + def get_louvain_clusters(nodes, edges): import networkx as nx import community - + G = nx.Graph() G.add_nodes_from(nodes) G.add_edges_from(edges) - + return np.array(community.best_partition(G).values()) + def load_edges(fname): edges = set([]) with open(fname) as f: for l in f: - edges.add(tuple(sorted(map(int,l.strip('\n').split(';'))))) + edges.add(tuple(sorted(map(int, l.strip('\n').split(';'))))) return edges + def build_categ_colors(categorical_coloring_data, cell_groupings): - for k,labels in cell_groupings.items(): - label_colors = {l:frac_to_hex(float(i)/len(set(labels))) for i,l in enumerate(list(set(labels)))} - categorical_coloring_data[k] = {'label_colors':label_colors, 'label_list':labels} + for k, labels in cell_groupings.items(): + label_colors = {l: frac_to_hex(float(i)/len(set(labels))) + for i, l in enumerate(list(set(labels)))} + categorical_coloring_data[k] = { + 'label_colors': label_colors, 'label_list': labels} return categorical_coloring_data + def save_cell_groupings(filename, categorical_coloring_data): - with open(filename,'w') as f: - f.write(json.dumps(categorical_coloring_data,indent=4, sort_keys=True).decode('utf-8')) + with open(filename, 'w') as f: + f.write(json.dumps(categorical_coloring_data, + indent=4, sort_keys=True).decode('utf-8')) #========================================================================================# - cwd = os.getcwd() if cwd.endswith('cgi-bin'): os.chdir('../') t00 = time.time() - - data = cgi.FieldStorage() base_dir = data.getvalue('base_dir') sub_dir = data.getvalue('sub_dir') @@ -68,11 +73,9 @@ def save_cell_groupings(filename, categorical_coloring_data): np.save(sub_dir + '/louvain_clusters.npy', clusts) -old_cell_groupings = json.load(open(sub_dir + '/categorical_coloring_data.json')) -new_cell_groupings = build_categ_colors(old_cell_groupings, {'Louvain cluster': map(str,clusts)}) -save_cell_groupings(sub_dir + '/categorical_coloring_data.json', new_cell_groupings) - - - - - +old_cell_groupings = json.load( + open(sub_dir + '/categorical_coloring_data.json')) +new_cell_groupings = build_categ_colors( + old_cell_groupings, {'Louvain cluster': map(str, clusts)}) +save_cell_groupings( + sub_dir + '/categorical_coloring_data.json', new_cell_groupings) diff --git a/cgi-bin/run_doublet_detector.py b/cgi-bin/run_doublet_detector.py index 171dc74..442f685 100755 --- a/cgi-bin/run_doublet_detector.py +++ b/cgi-bin/run_doublet_detector.py @@ -5,7 +5,7 @@ import os import json cgitb.enable() # for troubleshooting -print "Content-Type: text/plain\n" +print ("Content-Type: text/plain\n") #========================================================================================# @@ -135,7 +135,7 @@ def woublet(E=None, exp_doub_rate = 0.1, sim_doublet_ratio=3, k=50, use_approx_n # Check that input is valid if E is None and precomputed_pca is None: - print 'Please supply a counts matrix (E) or PCA coordinates (precomputed_pca)' + print ('Please supply a counts matrix (E) or PCA coordinates (precomputed_pca)') return # Convert counts matrix to sparse format if necessary @@ -195,7 +195,7 @@ def woublet(E=None, exp_doub_rate = 0.1, sim_doublet_ratio=3, k=50, use_approx_n total_counts = None del tmp else: - print 'Error: could not find "intermediates.npz"' + print ('Error: could not find "intermediates.npz"') doublet_scores, doublet_scores_sim, doub_neigh_parents = woublet(precomputed_pca = Epca, total_counts = total_counts, exp_doub_rate = f, sim_doublet_ratio = r, k = k, use_approx_nn = True, get_doub_parents = True) np.save(sub_dir + '/doublet_scores.npy', doublet_scores) diff --git a/cgi-bin/save_data.py b/cgi-bin/save_data.py index ef0cdb2..b32d13f 100755 --- a/cgi-bin/save_data.py +++ b/cgi-bin/save_data.py @@ -1,6 +1,9 @@ #!/usr/bin/env python import helper_functions -import cgi, cgitb, pickle, os +import cgi +import cgitb +import pickle +import os cgitb.enable() # for troubleshooting data = cgi.FieldStorage() @@ -14,16 +17,17 @@ content = data.getvalue('content') if filepath.endswith('coordinates.txt'): - if os.path.exists(filepath): - import datetime - dt = datetime.datetime.now().isoformat().replace(':','-').split('.')[0] - backup = filepath.replace('coordinates.txt','coordinates_'+dt+'.txt') - open(backup,'w').write(open(filepath).read()) + if os.path.exists(filepath): + import datetime + dt = datetime.datetime.now().isoformat().replace( + ':', '-').split('.')[0] + backup = filepath.replace('coordinates.txt', 'coordinates_'+dt+'.txt') + open(backup, 'w').write(open(filepath).read()) -open(filepath,'w').write(content) -if 'clustering_data' in filepath: - os.system('cp '+filepath+' '+filepath.replace('_clustmp','')) - os.system('rm -f '+filepath) +open(filepath, 'w').write(content) +if 'clustering_data' in filepath: + os.system('cp '+filepath+' '+filepath.replace('_clustmp', '')) + os.system('rm -f '+filepath) -print "Content-Type: text/html\n" -print 'sucess' +print("Content-Type: text/html\n") +print('sucess') diff --git a/cgi-bin/save_sticky.py b/cgi-bin/save_sticky.py index 82ec3f2..23025a7 100755 --- a/cgi-bin/save_sticky.py +++ b/cgi-bin/save_sticky.py @@ -1,6 +1,10 @@ #!/usr/bin/env python import helper_functions -import cgi, cgitb, pickle, os, json +import cgi +import cgitb +import pickle +import os +import json cgitb.enable() # for troubleshooting data = cgi.FieldStorage() @@ -10,26 +14,29 @@ os.chdir('../') -def check_same(d1,d2): - out = True - for k,v in d1.items(): - if not k in d2 or d2[k] != v: out = False - return out +def check_same(d1, d2): + out = True + for k, v in d1.items(): + if not k in d2 or d2[k] != v: + out = False + return out + filepath = data.getvalue('path') content = data.getvalue('content') if os.path.exists(filepath): - old_data = json.load(open(filepath)) - new_data = json.loads(content) - for d in new_data: - already_exists = False - for dd in old_data: - if check_same(d,dd): already_exists = True - if not already_exists: - old_data.append(d) - content = json.dumps(old_data) - -open(filepath,'w').write(content) - -print "Content-Type: text/html\n" -print 'sucess' + old_data = json.load(open(filepath)) + new_data = json.loads(content) + for d in new_data: + already_exists = False + for dd in old_data: + if check_same(d, dd): + already_exists = True + if not already_exists: + old_data.append(d) + content = json.dumps(old_data) + +open(filepath, 'w').write(content) + +print("Content-Type: text/html\n") +print('sucess') diff --git a/cgi-bin/smooth_gene.py b/cgi-bin/smooth_gene.py index 0bbedc2..7d123f8 100755 --- a/cgi-bin/smooth_gene.py +++ b/cgi-bin/smooth_gene.py @@ -1,107 +1,116 @@ #!/usr/bin/env python +import scipy.sparse as ssp +import numpy as np +import cgi import time import os cwd = os.getcwd() if cwd.endswith('cgi-bin'): os.chdir('../') - + t00 = time.time() + +print('BOP {}'.format(ssp)) + + def update_log(fname, logdat, overwrite=False): - if overwrite: - o = open(fname, 'w') - else: - o = open(fname, 'a') - o.write(logdat + '\n') - o.close() + if overwrite: + o = open(fname, 'w') + else: + o = open(fname, 'a') + o.write(logdat + '\n') + o.close() + def strfloat(x): - if x == 0: return "0" - else: return "%.1f" %x + if x == 0: + return '0' + else: + return '%.1f' % x def sparse_multiply(E, a): + import scipy.sparse as ssp nrow = E.shape[0] w = ssp.lil_matrix((nrow, nrow)) w.setdiag(a) return w * E + logf = 'tmplog2' t0 = time.time() -import cgi t1 = time.time() -update_log(logf, 'import cgi -- %.3f' %(t1-t0), True) +update_log(logf, 'import cgi -- %.3f' % (t1 - t0), True) t0 = time.time() -import numpy as np t1 = time.time() -update_log(logf, 'import numpy -- %.3f' %(t1-t0)) +update_log(logf, 'import numpy -- %.3f' % (t1 - t0)) t0 = time.time() -import scipy.sparse as ssp t1 = time.time() -update_log(logf, 'import scipy sparse -- %.3f' %(t1-t0)) +update_log(logf, 'import scipy sparse -- %.3f' % (t1 - t0)) + +print('Content-Type: text/plain\n') -print "Content-Type: text/plain" -print t0 = time.time() data = cgi.FieldStorage() base_dir = data.getvalue('base_dir') sub_dir = data.getvalue('sub_dir') -reds = np.array(map(float, data.getvalue('raw_r').split(',')))[:,None] -greens = np.array(map(float, data.getvalue('raw_g').split(',')))[:,None] -blues = np.array(map(float, data.getvalue('raw_b').split(',')))[:,None] +reds = np.array(list(map(float, data.getvalue('raw_r').split(','))))[:, None] +greens = np.array(list(map(float, data.getvalue('raw_g').split(','))))[:, None] +blues = np.array(list(map(float, data.getvalue('raw_b').split(','))))[:, None] E = np.hstack((reds, greens, blues)) -#E = np.array(map(float, data.getvalue('raw_g').split(',')))[:,None] +# E = np.array(map(float, data.getvalue('raw_g').split(',')))[:,None] sel = data.getvalue('selected')[1:] -print sel -if len(sel)==0: - sel = np.arange(E.shape[0]) -else: - sel = np.array(map(int, sel.split(',')),dtype=int) - E = E[sel,:] +print(sel) +if len(sel) == 0: + sel = np.arange(E.shape[0]) +else: + sel = np.array(map(int, sel.split(',')), dtype=int) + E = E[sel, :] beta = float(data.getvalue('beta')) n_rounds = int(data.getvalue('n_rounds')) t1 = time.time() -update_log(logf, 'got cgi data -- %.3f' %(t1-t0)) +update_log(logf, 'got cgi data -- %.3f' % (t1 - t0)) -##### SMOOTH +# SMOOTH t0 = time.time() try: - A = ssp.load_npz(sub_dir + '/A.npz') -except: - cell_filter = np.load(sub_dir + '/' + 'cell_filter.npy') - edges = np.loadtxt(sub_dir + '/edges.csv', delimiter=';',comments="") - A = ssp.lil_matrix((len(cell_filter), len(cell_filter))) - for iEdge in xrange(edges.shape[0]): - ii = edges[iEdge,0] - jj = edges[iEdge,1] - A[ii,jj] = 1 - A[jj,ii] = 1 - A = A.tocsc() - ssp.save_npz(sub_dir + '/A.npz', A) - + A = ssp.load_npz(sub_dir + '/A.npz') +except Exception: + cell_filter = np.load(sub_dir + '/' + 'cell_filter.npy') + edges = np.loadtxt(sub_dir + '/edges.csv', delimiter=';', comments='') + A = ssp.lil_matrix((len(cell_filter), len(cell_filter))) + for iEdge in range(edges.shape[0]): + ii = edges[iEdge, 0] + jj = edges[iEdge, 1] + A[ii, jj] = 1 + A[jj, ii] = 1 + A = A.tocsc() + ssp.save_npz(sub_dir + '/A.npz', A) + t1 = time.time() -update_log(logf, 'loaded adjacency matrix -- %.3f' %(t1-t0)) +update_log(logf, 'loaded adjacency matrix -- %.3f' % (t1 - t0)) ########### t0 = time.time() -A = A[:,sel].tocsr()[sel,:].tocsc() +A = A[:, sel].tocsr()[sel, :].tocsc() A = sparse_multiply(A, 1 / A.sum(1).A.squeeze()) -for iRound in xrange(n_rounds): - E = (beta * E + ((1 - beta) * A) * E) +for iRound in range(n_rounds): + E = (beta * E + ((1 - beta) * A) * E) t1 = time.time() -update_log(logf, 'smoothed data -- %.3f' %(t1-t0)) +update_log(logf, 'smoothed data -- %.3f' % (t1 - t0)) ########### # E = np.floor(E) @@ -109,9 +118,10 @@ def sparse_multiply(E, a): t0 = time.time() -print repr(np.min(E))+'|'+repr(np.max(E))+'|' +';'.join([','.join(map(str,E[:,0])), ','.join(map(str,E[:,1])), ','.join(map(str,E[:,2]))]) +print(repr(np.min(E)) + '|' + repr(np.max(E)) + '|' + ';'.join([','.join( + map(str, E[:, 0])), ','.join(map(str, E[:, 1])), ','.join(map(str, E[:, 2]))])) t1 = time.time() -update_log(logf, 'returned data -- %.3f' %(t1-t0)) +update_log(logf, 'returned data -- %.3f' % (t1 - t0)) t11 = time.time() -update_log(logf, str(t11-t00)) +update_log(logf, str(t11 - t00)) diff --git a/cgi-bin/spring_from_selection.execute.py b/cgi-bin/spring_from_selection.execute.py index 2048398..bf1daf7 100755 --- a/cgi-bin/spring_from_selection.execute.py +++ b/cgi-bin/spring_from_selection.execute.py @@ -26,12 +26,6 @@ t00 = time.time() -def sparse_var(E, axis=0): - mean_gene = E.mean(axis=axis).A.squeeze() - tmp = E.copy() - tmp.data **= 2 - return tmp.mean(axis=axis).A.squeeze() - mean_gene ** 2 - def update_log_html(fname, logdat, overwrite=False): if overwrite: o = open(fname, 'w') @@ -51,8 +45,8 @@ def update_log(fname, logdat, overwrite=False): def send_confirmation_email(email, name, info_dict, start_dataset, new_url): import smtplib - from email.MIMEMultipart import MIMEMultipart - from email.MIMEText import MIMEText + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText fromaddr = "singlecellSPRING@gmail.com" toaddr = email diff --git a/cgi-bin/spring_from_selection.tree.py b/cgi-bin/spring_from_selection.tree.py index 34b7837..02800c4 100755 --- a/cgi-bin/spring_from_selection.tree.py +++ b/cgi-bin/spring_from_selection.tree.py @@ -3,8 +3,7 @@ import cgi import cgitb cgitb.enable() # for troubleshooting -print "Content-Type: text/html" -print +print("Content-Type: text/html\n") import os import pickle @@ -30,7 +29,7 @@ compared_clusters = data.getvalue('compared_clusters') current_dir = base_dir + current_dir_short new_dir = base_dir + new_dir_short -this_url = 'https://kleintools.hms.harvard.edu/tools/springViewer_1_6_dev.html' +this_url = 'https://kleintools.hms.harvard.edu/tools/springViewer.html' all_errors = [] @@ -41,7 +40,8 @@ compared_clusters = '' if os.path.exists(new_dir + "/run_info.json"): - all_errors.append('A plot called "%s" already exists. Please enter a different name of plot.
' %new_dir_short) + all_errors.append( + 'A plot called "%s" already exists. Please enter a different name of plot.
' % new_dir_short) do_the_rest = False bad_chars = [" ", "/", "\\", ",", ":", "#", "\"", "\'"] @@ -54,64 +54,77 @@ else: found_bad.append(b) if len(found_bad) > 0: - all_errors.append('Enter a name of plot without the following characters: %s
' %(' '.join(found_bad))) + all_errors.append( + 'Enter a name of plot without the following characters: %s
' % (' '.join(found_bad))) # ERROR HANDLING try: user_email = data.getvalue('email') if "@" not in user_email: - all_errors.append('Enter a valid email address.
') + all_errors.append( + 'Enter a valid email address.
') do_the_rest = False except: - all_errors.append('Enter a valid email address.
') + all_errors.append( + 'Enter a valid email address.
') do_the_rest = False try: min_cells = int(data.getvalue('minCells')) except: - all_errors.append('Enter a number for min expressing cells.
') + all_errors.append( + 'Enter a number for min expressing cells.
') do_the_rest = False try: min_counts = float(data.getvalue('minCounts')) except: - all_errors.append('Enter a number for min number of UMIs.
') + all_errors.append( + 'Enter a number for min number of UMIs.
') do_the_rest = False try: min_vscore_pctl = float(data.getvalue('varPctl')) if min_vscore_pctl > 100 or min_vscore_pctl < 0: - all_errors.append('Enter a value 0-100 for gene variability.
') + all_errors.append( + 'Enter a value 0-100 for gene variability.
') do_the_rest = False except: - all_errors.append('Enter a value 0-100 for gene variability.
') + all_errors.append( + 'Enter a value 0-100 for gene variability.
') do_the_rest = False try: num_pc = int(data.getvalue('numPC')) if num_pc < 1: - all_errors.append('Enter an integer >0 for number of principal components.
') + all_errors.append( + 'Enter an integer >0 for number of principal components.
') do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of principal components.
') + all_errors.append( + 'Enter an integer >0 for number of principal components.
') do_the_rest = False try: k_neigh = int(data.getvalue('kneigh')) if k_neigh < 1: - all_errors.append('Enter an integer >0 for number of nearest neighbors.
') + all_errors.append( + 'Enter an integer >0 for number of nearest neighbors.
') do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of nearest neighbors.
') + all_errors.append( + 'Enter an integer >0 for number of nearest neighbors.
') do_the_rest = False try: num_fa2_iter = int(data.getvalue('nIter')) if num_fa2_iter < 1: - all_errors.append('Enter an integer >0 for number of force layout iterations.
') + all_errors.append( + 'Enter an integer >0 for number of force layout iterations.
') do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of force layout iterations.
') + all_errors.append( + 'Enter an integer >0 for number of force layout iterations.
') do_the_rest = False try: @@ -126,28 +139,30 @@ if not do_the_rest: - #os.rmdir(new_dir) - print 'Invalid input!
' + # os.rmdir(new_dir) + print('Invalid input!
') for err in all_errors: - print '> %s' %err + print('> %s' % err) else: try: - + if os.path.exists(new_dir): import shutil shutil.rmtree(new_dir) os.makedirs(new_dir) - - cat_dat = json.load(open(base_dir + '/base/categorical_coloring_data.json')) + + cat_dat = json.load( + open(base_dir + '/base/categorical_coloring_data.json')) clust_labels = np.array(cat_dat['Cluster Label']['label_list']) - all_clusters = np.array(selected_clusters.split(',')+compared_clusters.split(',')) + all_clusters = np.array(selected_clusters.split( + ',')+compared_clusters.split(',')) selected_clusters = np.array(selected_clusters.split(',')) - + base_filter = np.nonzero(np.in1d(clust_labels, selected_clusters))[0] extra_filter = np.nonzero(np.in1d(clust_labels, all_clusters))[0] base_ix = np.nonzero([(i in base_filter) for i in extra_filter])[0] - + params_dict = {} params_dict['extra_filter'] = extra_filter params_dict['base_ix'] = base_ix @@ -166,22 +181,21 @@ params_dict['description'] = description params_dict['user_email'] = user_email params_dict['animate'] = animate - + params_filename = new_dir + "/params.pickle" params_file = open(params_filename, 'wb') pickle.dump(params_dict, params_file, -1) params_file.close() - print 'Everything looks good. Now running...
' - print 'This could take a minute or two. Feel free to exit.
' - print 'You\'ll receive an email when your dataset is ready.
' + print('Everything looks good. Now running...
') + print('This could take a minute or two. Feel free to exit.
') + print('You\'ll receive an email when your dataset is ready.
') o = open(new_dir + '/lognewspring2.txt', 'w') o.write('Started processing
\n') o.close() subprocess.call(["cgi-bin/new_spring_submit.sh", new_dir]) - - except: - print 'Error starting processing!
' + except: + print('Error starting processing!
') diff --git a/cgi-bin/spring_from_selection2.online.py b/cgi-bin/spring_from_selection2.online.py index 5be9dae..57b576a 100755 --- a/cgi-bin/spring_from_selection2.online.py +++ b/cgi-bin/spring_from_selection2.online.py @@ -1,17 +1,16 @@ #!/usr/bin/env python +import subprocess +import numpy as np +import pickle +import os import cgi import cgitb cgitb.enable() # for troubleshooting -print "Content-Type: text/html" -import os -import pickle -import numpy as np -import subprocess -print '' +print("Content-Type: text/html\n") cwd = os.getcwd() if cwd.endswith('cgi-bin'): - os.chdir('../') + os.chdir('../') ##################### # CGI @@ -30,169 +29,184 @@ all_errors = [] if new_dir_short is None: - all_errors.append('Enter a name of plot
') - do_the_rest = False + all_errors.append('Enter a name of plot
') + do_the_rest = False else: - new_dir_short = new_dir_short.strip('/') - new_dir = base_dir + '/' + new_dir_short + new_dir_short = new_dir_short.strip('/') + new_dir = base_dir + '/' + new_dir_short if base_filter is None: - all_errors.append('No cells selected.
') - do_the_rest = False + all_errors.append('No cells selected.
') + do_the_rest = False if not new_dir_short is None: - if os.path.exists(new_dir + "/run_info.json"): - all_errors.append('A plot called "%s" already exists. Please enter a different name of plot.
' %new_dir_short) - do_the_rest = False + if os.path.exists(new_dir + "/run_info.json"): + all_errors.append( + 'A plot called "%s" already exists. Please enter a different name of plot.
' % new_dir_short) + do_the_rest = False bad_chars = [" ", "/", "\\", ",", ":", "#", "\"", "\'"] found_bad = [] if not new_dir_short is None: - for b in bad_chars: - if b in new_dir_short: - do_the_rest = False - if b == " ": - found_bad.append('space') - else: - found_bad.append(b) + for b in bad_chars: + if b in new_dir_short: + do_the_rest = False + if b == " ": + found_bad.append('space') + else: + found_bad.append(b) if len(found_bad) > 0: - all_errors.append('Enter a name of plot without the following characters: %s
' %(' '.join(found_bad))) + all_errors.append( + 'Enter a name of plot without the following characters: %s
' % (' '.join(found_bad))) # ERROR HANDLING try: - user_email = data.getvalue('email') - if "@" not in user_email: - all_errors.append('Enter a valid email address.
') - do_the_rest = False + user_email = data.getvalue('email') + if "@" not in user_email: + all_errors.append( + 'Enter a valid email address.
') + do_the_rest = False except: - user_email = '' + user_email = '' try: - min_cells = int(data.getvalue('minCells')) + min_cells = int(data.getvalue('minCells')) except: - all_errors.append('Enter a number for min expressing cells.
') - do_the_rest = False + all_errors.append( + 'Enter a number for min expressing cells.
') + do_the_rest = False try: - min_counts = float(data.getvalue('minCounts')) + min_counts = float(data.getvalue('minCounts')) except: - all_errors.append('Enter a number for min number of UMIs.
') - do_the_rest = False + all_errors.append( + 'Enter a number for min number of UMIs.
') + do_the_rest = False try: - min_vscore_pctl = float(data.getvalue('varPctl')) - if min_vscore_pctl > 100 or min_vscore_pctl < 0: - all_errors.append('Enter a value 0-100 for gene variability.
') - do_the_rest = False + min_vscore_pctl = float(data.getvalue('varPctl')) + if min_vscore_pctl > 100 or min_vscore_pctl < 0: + all_errors.append( + 'Enter a value 0-100 for gene variability.
') + do_the_rest = False except: - all_errors.append('Enter a value 0-100 for gene variability.
') - do_the_rest = False + all_errors.append( + 'Enter a value 0-100 for gene variability.
') + do_the_rest = False try: - num_pc = int(data.getvalue('numPC')) - if num_pc < 1: - all_errors.append('Enter an integer >0 for number of principal components.
') - do_the_rest = False + num_pc = int(data.getvalue('numPC')) + if num_pc < 1: + all_errors.append( + 'Enter an integer >0 for number of principal components.
') + do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of principal components.
') - do_the_rest = False + all_errors.append( + 'Enter an integer >0 for number of principal components.
') + do_the_rest = False try: - k_neigh = int(data.getvalue('kneigh')) - if k_neigh < 1: - all_errors.append('Enter an integer >0 for number of nearest neighbors.
') - do_the_rest = False + k_neigh = int(data.getvalue('kneigh')) + if k_neigh < 1: + all_errors.append( + 'Enter an integer >0 for number of nearest neighbors.
') + do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of nearest neighbors.
') - do_the_rest = False + all_errors.append( + 'Enter an integer >0 for number of nearest neighbors.
') + do_the_rest = False try: - num_fa2_iter = int(data.getvalue('nIter')) - if num_fa2_iter < 1: - all_errors.append('Enter an integer >0 for number of force layout iterations.
') - do_the_rest = False + num_fa2_iter = int(data.getvalue('nIter')) + if num_fa2_iter < 1: + all_errors.append( + 'Enter an integer >0 for number of force layout iterations.
') + do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of force layout iterations.
') - do_the_rest = False + all_errors.append( + 'Enter an integer >0 for number of force layout iterations.
') + do_the_rest = False try: - description = data.getvalue('description') + description = data.getvalue('description') except: - description = '' + description = '' try: - animate = data.getvalue('animate') + animate = data.getvalue('animate') except: - animate = 'No' + animate = 'No' try: - project_filter = data.getvalue('compared_cells') - project_filter = np.sort(np.array(map(int,project_filter.split(',')))) + project_filter = data.getvalue('compared_cells') + project_filter = np.sort(np.array(map(int, project_filter.split(',')))) except: - project_filter = np.array([]) + project_filter = np.array([]) try: - include_exclude = data.getvalue('include_exclude') - custom_genes = data.getvalue('custom_genes').replace('\r','\n').split('\n') + include_exclude = data.getvalue('include_exclude') + custom_genes = data.getvalue( + 'custom_genes').replace('\r', '\n').split('\n') except: - include_exclude = 'Exclude' - custom_genes = [] + include_exclude = 'Exclude' + custom_genes = [] if not do_the_rest: - #os.rmdir(new_dir) - print 'Invalid input!
' - for err in all_errors: - print '> %s' %err + # os.rmdir(new_dir) + print('Invalid input!
') + for err in all_errors: + print('> %s' % err) else: - try: - if os.path.exists(new_dir): - import shutil - shutil.rmtree(new_dir) - os.makedirs(new_dir) - - base_filter = np.sort(np.array(map(int,base_filter.split(',')))) - extra_filter = np.array(np.sort(np.hstack((base_filter,project_filter))),dtype=int) - base_ix = np.nonzero([(i in base_filter) for i in extra_filter])[0] - - params_dict = {} - params_dict['extra_filter'] = extra_filter - params_dict['base_ix'] = base_ix - params_dict['base_dir'] = base_dir - params_dict['current_dir'] = current_dir - params_dict['new_dir'] = new_dir - params_dict['current_dir_short'] = current_dir_short - params_dict['new_dir_short'] = new_dir_short - params_dict['min_vscore_pctl'] = min_vscore_pctl - params_dict['min_cells'] = min_cells - params_dict['min_counts'] = min_counts - params_dict['k_neigh'] = k_neigh - params_dict['num_pc'] = num_pc - params_dict['num_fa2_iter'] = num_fa2_iter - params_dict['this_url'] = this_url - params_dict['description'] = description - params_dict['user_email'] = user_email - params_dict['animate'] = animate - params_dict['include_exclude'] = include_exclude - params_dict['custom_genes'] = custom_genes - - params_filename = new_dir + "/params.pickle" - params_file = open(params_filename, 'wb') - pickle.dump(params_dict, params_file, -1) - params_file.close() - - print 'Everything looks good. Now running...
' - print 'This could take several minutes.
' - if user_email != '': print 'You will be notified of completion by email.
' - - o = open(new_dir + '/lognewspring2.txt', 'w') - o.write('Started processing
\n') - o.close() - - subprocess.call(["cgi-bin/new_spring_submit.sh", new_dir]) - - except: - print 'Error starting processing!
' - + try: + if os.path.exists(new_dir): + import shutil + shutil.rmtree(new_dir) + os.makedirs(new_dir) + + base_filter = np.sort(np.array(map(int, base_filter.split(',')))) + extra_filter = np.array( + np.sort(np.hstack((base_filter, project_filter))), dtype=int) + base_ix = np.nonzero([(i in base_filter) for i in extra_filter])[0] + + params_dict = {} + params_dict['extra_filter'] = extra_filter + params_dict['base_ix'] = base_ix + params_dict['base_dir'] = base_dir + params_dict['current_dir'] = current_dir + params_dict['new_dir'] = new_dir + params_dict['current_dir_short'] = current_dir_short + params_dict['new_dir_short'] = new_dir_short + params_dict['min_vscore_pctl'] = min_vscore_pctl + params_dict['min_cells'] = min_cells + params_dict['min_counts'] = min_counts + params_dict['k_neigh'] = k_neigh + params_dict['num_pc'] = num_pc + params_dict['num_fa2_iter'] = num_fa2_iter + params_dict['this_url'] = this_url + params_dict['description'] = description + params_dict['user_email'] = user_email + params_dict['animate'] = animate + params_dict['include_exclude'] = include_exclude + params_dict['custom_genes'] = custom_genes + + params_filename = new_dir + "/params.pickle" + params_file = open(params_filename, 'wb') + pickle.dump(params_dict, params_file, -1) + params_file.close() + + print('Everything looks good. Now running...
') + print('This could take several minutes.
') + if user_email != '': + print('You will be notified of completion by email.
') + + o = open(new_dir + '/lognewspring2.txt', 'w') + o.write('Started processing
\n') + o.close() + + subprocess.call(["cgi-bin/new_spring_submit.sh", new_dir]) + + except: + print('Error starting processing!
') diff --git a/cgi-bin/spring_from_selection2.py b/cgi-bin/spring_from_selection2.py index b745504..897dc7c 100755 --- a/cgi-bin/spring_from_selection2.py +++ b/cgi-bin/spring_from_selection2.py @@ -2,8 +2,7 @@ import cgi import cgitb cgitb.enable() # for troubleshooting -print "Content-Type: text/html" -print +print("Content-Type: text/html\n") import os import pickle import numpy as np @@ -13,7 +12,7 @@ cwd = os.getcwd() if cwd.endswith('cgi-bin'): - os.chdir('../') + os.chdir('../') ##################### # CGI @@ -32,171 +31,187 @@ all_errors = [] if new_dir_short is None: - all_errors.append('Enter a name of plot
') - do_the_rest = False + all_errors.append('Enter a name of plot
') + do_the_rest = False else: - new_dir_short = new_dir_short.strip('/') - new_dir = base_dir + '/' + new_dir_short + new_dir_short = new_dir_short.strip('/') + new_dir = base_dir + '/' + new_dir_short if base_filter is None: - all_errors.append('No cells selected.
') - do_the_rest = False + all_errors.append('No cells selected.
') + do_the_rest = False if not new_dir_short is None: - if os.path.exists(new_dir + "/run_info.json"): - all_errors.append('A plot called "%s" already exists. Please enter a different name of plot.
' %new_dir_short) - do_the_rest = False + if os.path.exists(new_dir + "/run_info.json"): + all_errors.append( + 'A plot called "%s" already exists. Please enter a different name of plot.
' % new_dir_short) + do_the_rest = False bad_chars = [" ", "/", "\\", ",", ":", "#", "\"", "\'"] found_bad = [] if not new_dir_short is None: - for b in bad_chars: - if b in new_dir_short: - do_the_rest = False - if b == " ": - found_bad.append('space') - else: - found_bad.append(b) + for b in bad_chars: + if b in new_dir_short: + do_the_rest = False + if b == " ": + found_bad.append('space') + else: + found_bad.append(b) if len(found_bad) > 0: - all_errors.append('Enter a name of plot without the following characters: %s
' %(' '.join(found_bad))) + all_errors.append( + 'Enter a name of plot without the following characters: %s
' % (' '.join(found_bad))) # ERROR HANDLING try: - user_email = data.getvalue('email') - if "@" not in user_email: - all_errors.append('Enter a valid email address.
') - do_the_rest = False + user_email = data.getvalue('email') + if "@" not in user_email: + all_errors.append( + 'Enter a valid email address.
') + do_the_rest = False except: - user_email = '' + user_email = '' try: - min_cells = int(data.getvalue('minCells')) + min_cells = int(data.getvalue('minCells')) except: - all_errors.append('Enter a number for min expressing cells.
') - do_the_rest = False + all_errors.append( + 'Enter a number for min expressing cells.
') + do_the_rest = False try: - min_counts = float(data.getvalue('minCounts')) + min_counts = float(data.getvalue('minCounts')) except: - all_errors.append('Enter a number for min number of UMIs.
') - do_the_rest = False + all_errors.append( + 'Enter a number for min number of UMIs.
') + do_the_rest = False try: - min_vscore_pctl = float(data.getvalue('varPctl')) - if min_vscore_pctl > 100 or min_vscore_pctl < 0: - all_errors.append('Enter a value 0-100 for gene variability.
') - do_the_rest = False + min_vscore_pctl = float(data.getvalue('varPctl')) + if min_vscore_pctl > 100 or min_vscore_pctl < 0: + all_errors.append( + 'Enter a value 0-100 for gene variability.
') + do_the_rest = False except: - all_errors.append('Enter a value 0-100 for gene variability.
') - do_the_rest = False + all_errors.append( + 'Enter a value 0-100 for gene variability.
') + do_the_rest = False try: - num_pc = int(data.getvalue('numPC')) - if num_pc < 1: - all_errors.append('Enter an integer >0 for number of principal components.
') - do_the_rest = False + num_pc = int(data.getvalue('numPC')) + if num_pc < 1: + all_errors.append( + 'Enter an integer >0 for number of principal components.
') + do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of principal components.
') - do_the_rest = False + all_errors.append( + 'Enter an integer >0 for number of principal components.
') + do_the_rest = False try: - k_neigh = int(data.getvalue('kneigh')) - if k_neigh < 1: - all_errors.append('Enter an integer >0 for number of nearest neighbors.
') - do_the_rest = False + k_neigh = int(data.getvalue('kneigh')) + if k_neigh < 1: + all_errors.append( + 'Enter an integer >0 for number of nearest neighbors.
') + do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of nearest neighbors.
') - do_the_rest = False + all_errors.append( + 'Enter an integer >0 for number of nearest neighbors.
') + do_the_rest = False try: - num_fa2_iter = int(data.getvalue('nIter')) - if num_fa2_iter < 1: - all_errors.append('Enter an integer >0 for number of force layout iterations.
') - do_the_rest = False + num_fa2_iter = int(data.getvalue('nIter')) + if num_fa2_iter < 1: + all_errors.append( + 'Enter an integer >0 for number of force layout iterations.
') + do_the_rest = False except: - all_errors.append('Enter an integer >0 for number of force layout iterations.
') - do_the_rest = False + all_errors.append( + 'Enter an integer >0 for number of force layout iterations.
') + do_the_rest = False try: - description = data.getvalue('description') + description = data.getvalue('description') except: - description = '' + description = '' try: - animate = data.getvalue('animate') + animate = data.getvalue('animate') except: - animate = 'No' + animate = 'No' try: - project_filter = data.getvalue('compared_cells') - project_filter = np.sort(np.array(map(int,project_filter.split(',')))) + project_filter = data.getvalue('compared_cells') + project_filter = np.sort(np.array(map(int, project_filter.split(',')))) except: - project_filter = np.array([]) - + project_filter = np.array([]) + try: - include_exclude = data.getvalue('include_exclude') - custom_genes = data.getvalue('custom_genes').replace('\r','\n').split('\n') + include_exclude = data.getvalue('include_exclude') + custom_genes = data.getvalue( + 'custom_genes').replace('\r', '\n').split('\n') except: - include_exclude = 'Exclude' - custom_genes = [] + include_exclude = 'Exclude' + custom_genes = [] if not do_the_rest: - #os.rmdir(new_dir) - print 'Invalid input!
' - for err in all_errors: - print '> %s' %err + # os.rmdir(new_dir) + print('Invalid input!
') + for err in all_errors: + print('> %s' % err) else: - try: - if os.path.exists(new_dir): - import shutil - shutil.rmtree(new_dir) - os.makedirs(new_dir) - - base_filter = np.sort(np.array(map(int,base_filter.split(',')))) - extra_filter = np.array(np.sort(np.hstack((base_filter,project_filter))),dtype=int) - base_ix = np.nonzero([(i in base_filter) for i in extra_filter])[0] - - params_dict = {} - params_dict['extra_filter'] = extra_filter - params_dict['base_ix'] = base_ix - params_dict['base_dir'] = base_dir - params_dict['current_dir'] = current_dir - params_dict['new_dir'] = new_dir - params_dict['current_dir_short'] = current_dir_short - params_dict['new_dir_short'] = new_dir_short - params_dict['min_vscore_pctl'] = min_vscore_pctl - params_dict['min_cells'] = min_cells - params_dict['min_counts'] = min_counts - params_dict['k_neigh'] = k_neigh - params_dict['num_pc'] = num_pc - params_dict['num_fa2_iter'] = num_fa2_iter - params_dict['this_url'] = this_url - params_dict['description'] = description - params_dict['user_email'] = user_email - params_dict['animate'] = animate - params_dict['include_exclude'] = include_exclude - params_dict['custom_genes'] = custom_genes - - params_filename = new_dir + "/params.pickle" - params_file = open(params_filename, 'wb') - pickle.dump(params_dict, params_file, -1) - params_file.close() - - #print 'Everything looks good. Now running...
' - #print 'This could take several minutes.
' - #if user_email != '': print 'You will be notified of completion by email.
' - - o = open(new_dir + '/lognewspring2.txt', 'w') - o.write('Processing...
\n') - o.close() - - output_message = spring_from_selection_execute.execute_spring(params_filename) - print output_message - - except: - print 'Error starting processing!
' + try: + if os.path.exists(new_dir): + import shutil + shutil.rmtree(new_dir) + os.makedirs(new_dir) + + base_filter = np.sort(np.array(map(int, base_filter.split(',')))) + extra_filter = np.array( + np.sort(np.hstack((base_filter, project_filter))), dtype=int) + base_ix = np.nonzero([(i in base_filter) for i in extra_filter])[0] + + params_dict = {} + params_dict['extra_filter'] = extra_filter + params_dict['base_ix'] = base_ix + params_dict['base_dir'] = base_dir + params_dict['current_dir'] = current_dir + params_dict['new_dir'] = new_dir + params_dict['current_dir_short'] = current_dir_short + params_dict['new_dir_short'] = new_dir_short + params_dict['min_vscore_pctl'] = min_vscore_pctl + params_dict['min_cells'] = min_cells + params_dict['min_counts'] = min_counts + params_dict['k_neigh'] = k_neigh + params_dict['num_pc'] = num_pc + params_dict['num_fa2_iter'] = num_fa2_iter + params_dict['this_url'] = this_url + params_dict['description'] = description + params_dict['user_email'] = user_email + params_dict['animate'] = animate + params_dict['include_exclude'] = include_exclude + params_dict['custom_genes'] = custom_genes + + params_filename = new_dir + "/params.pickle" + params_file = open(params_filename, 'wb') + pickle.dump(params_dict, params_file, -1) + params_file.close() + + # print 'Everything looks good. Now running...
' + # print 'This could take several minutes.
' + # if user_email != '': print 'You will be notified of completion by email.
' + + o = open(new_dir + '/lognewspring2.txt', 'w') + o.write('Processing...
\n') + o.close() + + output_message = spring_from_selection_execute.execute_spring( + params_filename) + print(output_message) + + except: + print('Error starting processing!
') diff --git a/cgi-bin/spring_from_selection_execute.online.py b/cgi-bin/spring_from_selection_execute.online.py index d64883a..0ee198c 100755 --- a/cgi-bin/spring_from_selection_execute.online.py +++ b/cgi-bin/spring_from_selection_execute.online.py @@ -11,11 +11,6 @@ import pickle import datetime -def sparse_var(E, axis=0): - mean_gene = E.mean(axis=axis).A.squeeze() - tmp = E.copy() - tmp.data **= 2 - return tmp.mean(axis=axis).A.squeeze() - mean_gene ** 2 def update_log_html(fname, logdat, overwrite=False): if overwrite: @@ -25,6 +20,7 @@ def update_log_html(fname, logdat, overwrite=False): o.write(logdat + '
\n') o.close() + def update_log(fname, logdat, overwrite=False): if overwrite: o = open(fname, 'w') @@ -33,11 +29,12 @@ def update_log(fname, logdat, overwrite=False): o.write(logdat + '\n') o.close() + def send_confirmation_email(email, name, info_dict, start_dataset, new_url): import smtplib - from email.MIMEMultipart import MIMEMultipart - from email.MIMEText import MIMEText + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText fromaddr = "singlecellSPRING@gmail.com" toaddr = email @@ -46,15 +43,23 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): msg['To'] = toaddr msg['Subject'] = 'SPRING is finished processing '+name - body = 'SPRING finished processing your dataset '+name+' using the following parameters:\n\n' + body = 'SPRING finished processing your dataset ' + \ + name+' using the following parameters:\n\n' body += 'Starting dataset ' + start_dataset + '\n' - body += 'Min expressing cells (gene filtering): ' + str(info_dict['Min_Cells']) + '\n' - body += 'Min number of UMIs (gene filtering): ' + str(info_dict['Min_Counts']) + '\n' - body += 'Gene variability percentile (gene filtering): ' + str(info_dict['Gene_Var_Pctl']) + '\n' - body += 'Number of principal components: ' + str(info_dict['Num_PCs']) + '\n' - body += 'Number of nearest neighbors: ' + str(info_dict['Num_Neighbors']) + '\n' - body += 'Number of force layout iterations: ' + str(info_dict['Num_Force_Iter']) + '\n\n' - body += 'Used %i cells and %i genes to build the SPRING plot.\n\n' %(info_dict['Nodes'], info_dict['Filtered_Genes']) + body += 'Min expressing cells (gene filtering): ' + \ + str(info_dict['Min_Cells']) + '\n' + body += 'Min number of UMIs (gene filtering): ' + \ + str(info_dict['Min_Counts']) + '\n' + body += 'Gene variability percentile (gene filtering): ' + \ + str(info_dict['Gene_Var_Pctl']) + '\n' + body += 'Number of principal components: ' + \ + str(info_dict['Num_PCs']) + '\n' + body += 'Number of nearest neighbors: ' + \ + str(info_dict['Num_Neighbors']) + '\n' + body += 'Number of force layout iterations: ' + \ + str(info_dict['Num_Force_Iter']) + '\n\n' + body += 'Used %i cells and %i genes to build the SPRING plot.\n\n' % ( + info_dict['Nodes'], info_dict['Filtered_Genes']) body += 'You can view the results at\n' + new_url + '\n' msg.attach(MIMEText(body, 'plain')) @@ -65,6 +70,7 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): server.sendmail(fromaddr, toaddr, text) server.quit() + try: from fa2_anim import ForceAtlas2 animation_mode = True @@ -110,27 +116,30 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): timef = new_dir + '/lognewspringtime.txt' - ####################### # Load data cell_filter = np.load(current_dir + '/cell_filter.npy')[extra_filter] np.save(new_dir + '/cell_filter.npy', cell_filter) np.savetxt(new_dir + '/cell_filter.txt', cell_filter, fmt='%i') -gene_list = np.loadtxt(base_dir + '/genes.txt', dtype=str, delimiter='\t', comments="") +gene_list = np.loadtxt(base_dir + '/genes.txt', + dtype=str, delimiter='\t', comments="") prefix_map = {} -for g in gene_list: prefix_map[g.split()[0]] = g -for g in gene_list: prefix_map[g.split()[-1]] = g -custom_genes = set([prefix_map[g] for g in custom_genes if g in prefix_map]+[g for g in custom_genes if g in gene_list]) +for g in gene_list: + prefix_map[g.split()[0]] = g +for g in gene_list: + prefix_map[g.split()[-1]] = g +custom_genes = set([prefix_map[g] for g in custom_genes if g in prefix_map] + + [g for g in custom_genes if g in gene_list]) t0 = time.time() update_log_html(logf, 'Loading counts data...') E = ssp.load_npz(base_dir + '/counts_norm.npz') -E = E[cell_filter,:] +E = E[cell_filter, :] if not ssp.isspmatrix_csc(E): E = E.tocsc() t1 = time.time() -update_log(timef, 'Counts loaded from npz -- %.2f' %(t1-t0), True) +update_log(timef, 'Counts loaded from npz -- %.2f' % (t1-t0), True) ################ @@ -148,13 +157,15 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): for iG in range(E.shape[1]): n_nonzero = E.indptr[iG+1] - E.indptr[iG] if n_nonzero > pctl_n: - pctls[iG] = np.percentile(E.data[E.indptr[iG]:E.indptr[iG+1]], 100 - 100 * pctl_n / n_nonzero) + pctls[iG] = np.percentile( + E.data[E.indptr[iG]:E.indptr[iG+1]], 100 - 100 * pctl_n / n_nonzero) else: pctls[iG] = 0 - color_stats[gene_list[iG]] = tuple(map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG]))) + color_stats[gene_list[iG]] = tuple( + map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG]))) t1 = time.time() -update_log(timef, 'Stats computed -- %.2f' %(t1-t0)) +update_log(timef, 'Stats computed -- %.2f' % (t1-t0)) ################ # Save color stats, custom colors @@ -163,79 +174,86 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): custom_colors = {} f = open(current_dir + '/color_data_gene_sets.csv', 'r') for l in f: - print l[:100] + print(l[:100]) cols = l.strip('\n').split(',') custom_colors[cols[0]] = map(float, np.array(cols[1:])[extra_filter]) -for k,v in custom_colors.items(): - color_stats[k] = (0,1,np.min(v),np.max(v)+.01,np.percentile(v,99)) -with open(new_dir+'/color_stats.json','w') as f: - f.write(json.dumps(color_stats,indent=4, sort_keys=True).decode('utf-8')) -with open(new_dir+'/color_data_gene_sets.csv','w') as f: - for k,v in custom_colors.items(): +for k, v in custom_colors.items(): + color_stats[k] = (0, 1, np.min(v), np.max(v)+.01, np.percentile(v, 99)) +with open(new_dir+'/color_stats.json', 'w') as f: + f.write(json.dumps(color_stats, indent=4, sort_keys=True).decode('utf-8')) +with open(new_dir+'/color_data_gene_sets.csv', 'w') as f: + for k, v in custom_colors.items(): f.write(k + ',' + ','.join(map(str, v)) + '\n') t1 = time.time() -update_log(timef, 'Saved color stats -- %.2f' %(t1-t0)) +update_log(timef, 'Saved color stats -- %.2f' % (t1-t0)) ############### # Save cell groupings t0 = time.time() update_log_html(logf, 'Saving cell labels...') -cell_groupings = json.load(open(current_dir + '/categorical_coloring_data.json')) +cell_groupings = json.load( + open(current_dir + '/categorical_coloring_data.json')) new_cell_groupings = {} if len(cell_groupings) > 0: for k in cell_groupings: new_cell_groupings[k] = {} - new_cell_groupings[k]['label_list'] = [str(cell_groupings[k]['label_list'][i]) for i in extra_filter] + new_cell_groupings[k]['label_list'] = [ + str(cell_groupings[k]['label_list'][i]) for i in extra_filter] uniq_groups = np.unique(np.array(new_cell_groupings[k]['label_list'])) new_cell_groupings[k]['label_colors'] = {} for kk in uniq_groups: new_cell_groupings[k]['label_colors'][kk] = cell_groupings[k]['label_colors'][kk] -with open(new_dir+'/categorical_coloring_data.json','w') as f: - f.write(json.dumps(new_cell_groupings,indent=4, sort_keys=True).decode('utf-8')) +with open(new_dir+'/categorical_coloring_data.json', 'w') as f: + f.write(json.dumps(new_cell_groupings, indent=4, + sort_keys=True).decode('utf-8')) t1 = time.time() -update_log(timef, 'Saved cell labels -- %.2f' %(t1-t0)) +update_log(timef, 'Saved cell labels -- %.2f' % (t1-t0)) ################ # Gene filtering t0 = time.time() update_log_html(logf, 'Filtering genes...') if (min_counts > 0) or (min_cells > 0) or (min_vscore_pctl > 0): - gene_filter = filter_genes(E[base_ix,:], min_counts, min_cells, min_vscore_pctl) + gene_filter = filter_genes( + E[base_ix, :], min_counts, min_cells, min_vscore_pctl) else: gene_filter = np.arange(E.shape[1]) if include_exclude == 'Exclude': - gene_filter = np.array([i for i in gene_filter if not gene_list[i] in custom_genes]) + gene_filter = np.array( + [i for i in gene_filter if not gene_list[i] in custom_genes]) else: - gene_filter = np.array([i for i in gene_filter if gene_list[i] in custom_genes]) + gene_filter = np.array( + [i for i in gene_filter if gene_list[i] in custom_genes]) -if len(gene_filter)==0: +if len(gene_filter) == 0: update_log_html(logf, 'Error: No genes survived filtering') - print 'Error: No genes survived filtering' + print('Error: No genes survived filtering') sys.exit() - + t1 = time.time() -update_log(timef, 'Using %i genes -- %.2f' %(len(gene_filter), t1-t0)) +update_log(timef, 'Using %i genes -- %.2f' % (len(gene_filter), t1-t0)) -with open(new_dir+'/filtered_genes.tsv','w') as o: +with open(new_dir+'/filtered_genes.tsv', 'w') as o: o.write('gene_index\tgene_name\n') for iG in gene_filter: - o.write('%i\t%s\n' %(iG, gene_list[iG])) + o.write('%i\t%s\n' % (iG, gene_list[iG])) ################ # PCA t0 = time.time() update_log_html(logf, 'Running PCA...') -num_pc = np.min([num_pc,len(gene_filter)]) +num_pc = np.min([num_pc, len(gene_filter)]) if E.shape[0] > 50000: pca_method = 'sparse' else: pca_method = '' -Epca = get_PCA_sparseInput(E[:,gene_filter], numpc=num_pc, method=pca_method, base_ix=base_ix) +Epca = get_PCA_sparseInput( + E[:, gene_filter], numpc=num_pc, method=pca_method, base_ix=base_ix) t1 = time.time() -update_log(timef, 'PCA done -- %.2f' %(t1-t0)) +update_log(timef, 'PCA done -- %.2f' % (t1-t0)) np.savetxt(new_dir+'/pca.csv', Epca, delimiter=',') @@ -247,25 +265,27 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): approx = True else: approx = False -links, knn_graph = get_knn_graph2(Epca, k=k_neigh, dist_metric = 'euclidean', approx=approx) +links, knn_graph = get_knn_graph2( + Epca, k=k_neigh, dist_metric='euclidean', approx=approx) links = list(links) t1 = time.time() -update_log(timef, 'KNN built -- %.2f' %(t1-t0)) +update_log(timef, 'KNN built -- %.2f' % (t1-t0)) ################ # Save graph data t0 = time.time() update_log_html(logf, 'Saving graph...') -nodes = [{'name':int(i),'number':int(i)} for i in range(E.shape[0])] -edges = [{'source':int(i), 'target':int(j), 'distance':0} for i,j in links] -out = {'nodes':nodes,'links':edges} -open(new_dir+'/graph_data.json','w').write(json.dumps(out,indent=4, separators=(',', ': '))) +nodes = [{'name': int(i), 'number': int(i)} for i in range(E.shape[0])] +edges = [{'source': int(i), 'target': int(j), 'distance': 0} for i, j in links] +out = {'nodes': nodes, 'links': edges} +open(new_dir+'/graph_data.json', + 'w').write(json.dumps(out, indent=4, separators=(',', ': '))) edgef = open(new_dir+'/edges.csv', 'w') for ee in links: - edgef.write('%i;%i\n' %(ee[0], ee[1]) ) + edgef.write('%i;%i\n' % (ee[0], ee[1])) edgef.close() t1 = time.time() -update_log(timef, 'Graph data saved -- %.2f' %(t1-t0)) +update_log(timef, 'Graph data saved -- %.2f' % (t1-t0)) # update_log_html(logf, str(knn_graph.shape)) ################ @@ -277,19 +297,20 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): G.add_edges_from(links) forceatlas2 = ForceAtlas2( - # Behavior alternatives - outboundAttractionDistribution=False, # Dissuade hubs - linLogMode=False, # NOT IMPLEMENTED - adjustSizes=False, # Prevent overlap (NOT IMPLEMENTED) - edgeWeightInfluence=1.0, - - # Performance - jitterTolerance=1.0, # Tolerance - barnesHutOptimize=True, - barnesHutTheta=2, - multiThreaded=False, # NOT IMPLEMENTED - - # Tuning + # Behavior alternatives + outboundAttractionDistribution=False, # Dissuade hubs + linLogMode=False, # NOT IMPLEMENTED + # Prevent overlap (NOT IMPLEMENTED) + adjustSizes=False, + edgeWeightInfluence=1.0, + + # Performance + jitterTolerance=1.0, # Tolerance + barnesHutOptimize=True, + barnesHutTheta=2, + multiThreaded=False, # NOT IMPLEMENTED + + # Tuning scalingRatio=1.0, strongGravityMode=False, gravity=0.05, @@ -297,62 +318,69 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): # Log verbose=False) -if animation_mode and animate=='Yes': - f = open(new_dir+'/animation.txt','w') - f = open(new_dir+'/animation.txt','a') - positions = forceatlas2.forceatlas2_networkx_layout(G, pos=None, iterations=num_fa2_iter, writefile=f) +if animation_mode and animate == 'Yes': + f = open(new_dir+'/animation.txt', 'w') + f = open(new_dir+'/animation.txt', 'a') + positions = forceatlas2.forceatlas2_networkx_layout( + G, pos=None, iterations=num_fa2_iter, writefile=f) else: - positions = forceatlas2.forceatlas2_networkx_layout(G, pos=None, iterations=num_fa2_iter) + positions = forceatlas2.forceatlas2_networkx_layout( + G, pos=None, iterations=num_fa2_iter) positions = np.array([positions[i] for i in sorted(positions.keys())]) positions = positions / 5.0 -positions = positions - np.min(positions, axis = 0) - np.ptp(positions, axis = 0) / 2.0 -positions[:,0] = positions[:,0] + 750 -positions[:,1] = positions[:,1] + 250 +positions = positions - np.min(positions, axis=0) - \ + np.ptp(positions, axis=0) / 2.0 +positions[:, 0] = positions[:, 0] + 750 +positions[:, 1] = positions[:, 1] + 250 t1 = time.time() -update_log(timef, 'Ran ForceAtlas2 -- %.2f' %(t1-t0)) +update_log(timef, 'Ran ForceAtlas2 -- %.2f' % (t1-t0)) ################ # Save coordinates base_name = base_dir.strip('/').split('/')[-1] new_name = new_dir.strip('/').split('/')[-1] -np.savetxt(new_dir + '/' + 'coordinates.txt', np.hstack((np.arange(E.shape[0])[:,None], positions)), fmt='%i,%.5f,%.5f') +np.savetxt(new_dir + '/' + 'coordinates.txt', + np.hstack((np.arange(E.shape[0])[:, None], positions)), fmt='%i,%.5f,%.5f') ################ # Save new clone data if it exists in base dir if os.path.exists(current_dir + '/clone_map.json'): clone_map_dict = json.load(open(current_dir + '/clone_map.json')) - extra_filter_map = {i:j for j,i in enumerate(extra_filter)} + extra_filter_map = {i: j for j, i in enumerate(extra_filter)} new_clone_map_dict = {} for k, clone_map in clone_map_dict.items(): new_clone_map = {} - for i,clone in clone_map.items(): + for i, clone in clone_map.items(): i = int(i) - new_clone = [extra_filter_map[j] for j in clone if j in extra_filter_map] + new_clone = [extra_filter_map[j] + for j in clone if j in extra_filter_map] if i in extra_filter_map and len(new_clone) > 0: new_clone_map[extra_filter_map[i]] = new_clone new_clone_map_dict[k] = new_clone_map - json.dump(new_clone_map_dict,open(new_dir+'/clone_map.json','w')) + json.dump(new_clone_map_dict, open(new_dir+'/clone_map.json', 'w')) ################ # Save PCA, gene filter, total counts if os.path.exists(base_dir + '/total_counts.txt'): - total_counts = np.loadtxt(base_dir + '/total_counts.txt', comments="")[cell_filter] - np.savez_compressed(new_dir + '/intermediates.npz', Epca = Epca, gene_filter = gene_filter, total_counts = total_counts) + total_counts = np.loadtxt( + base_dir + '/total_counts.txt', comments="")[cell_filter] + np.savez_compressed(new_dir + '/intermediates.npz', Epca=Epca, + gene_filter=gene_filter, total_counts=total_counts) else: - np.savez_compressed(new_dir + '/intermediates.npz', Epca = Epca, gene_filter = gene_filter) + np.savez_compressed(new_dir + '/intermediates.npz', + Epca=Epca, gene_filter=gene_filter) + - ################ # Save run info -import datetime info_dict = {} info_dict['Email'] = user_email -info_dict['Date'] = '%s' %creation_time +info_dict['Date'] = '%s' % creation_time info_dict['Nodes'] = Epca.shape[0] info_dict['Filtered_Genes'] = len(gene_filter) info_dict['Gene_Var_Pctl'] = min_vscore_pctl @@ -364,15 +392,17 @@ def send_confirmation_email(email, name, info_dict, start_dataset, new_url): info_dict['Description'] = description start_dataset = base_name + '/' + current_dir_short -with open(new_dir+'/run_info.json','w') as f: - f.write(json.dumps(info_dict,indent=4, sort_keys=True).decode('utf-8')) +with open(new_dir+'/run_info.json', 'w') as f: + f.write(json.dumps(info_dict, indent=4, sort_keys=True).decode('utf-8')) ################ t11 = time.time() url_pref = this_url.split('?')[0] -update_log_html(logf, 'Run complete! Done in %i seconds.
' %(t11-t00) + ' Click here to view.' %(url_pref,new_dir.strip('/'))) +update_log_html(logf, 'Run complete! Done in %i seconds.
' % ( + t11-t00) + ' Click here to view.' % (url_pref, new_dir.strip('/'))) if user_email != '': new_url_full = url_pref + '?' + new_dir.strip('/') - send_confirmation_email(user_email, base_name + '/' + new_name, info_dict, start_dataset, new_url_full) + send_confirmation_email(user_email, base_name + '/' + + new_name, info_dict, start_dataset, new_url_full) ################ diff --git a/cgi-bin/spring_from_selection_execute.py b/cgi-bin/spring_from_selection_execute.py index 2b60697..8009c5d 100755 --- a/cgi-bin/spring_from_selection_execute.py +++ b/cgi-bin/spring_from_selection_execute.py @@ -1,5 +1,7 @@ #!/usr/bin/env python +from wolkit import * + def sparse_var(E, axis=0): mean_gene = E.mean(axis=axis).A.squeeze() tmp = E.copy() @@ -25,8 +27,8 @@ def update_log(fname, logdat, overwrite=False): def send_confirmation_email(email, name, info_dict, start_dataset, new_url): import smtplib - from email.MIMEMultipart import MIMEMultipart - from email.MIMEText import MIMEText + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText fromaddr = "singlecellSPRING@gmail.com" toaddr = email @@ -67,7 +69,6 @@ def execute_spring(param_filename): import h5py import json import time - from wolkit import * import networkx as nx import pickle import datetime @@ -229,7 +230,7 @@ def execute_spring(param_filename): gene_filter = np.array([i for i in gene_filter if gene_list[i] in custom_genes]) if len(gene_filter)==0: - print 'Error: No genes survived filtering' + print ('Error: No genes survived filtering') sys.exit() t1 = time.time() diff --git a/currentDatasetsList.html b/currentDatasetsList.html index 495b866..6f044e5 100644 --- a/currentDatasetsList.html +++ b/currentDatasetsList.html @@ -2,9 +2,9 @@ - - - + + + @@ -18,10 +18,10 @@

- - - - + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/springViewer_1_5_dev.html b/springViewer_1_5_dev.html deleted file mode 100755 index 3caba69..0000000 --- a/springViewer_1_5_dev.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - -
-
-
- -
- -
- -
-
- -
- - - - - - - -
-
-
-
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - diff --git a/springViewer_1_6_dev.html b/springViewer_1_6_dev.html deleted file mode 100755 index ee68922..0000000 --- a/springViewer_1_6_dev.html +++ /dev/null @@ -1,254 +0,0 @@ - - - - - -
-
- - - - - - -
- -
- - -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
- -
- - -
-
-
-
- - - -
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/LineSprite.js b/src/LineSprite.js new file mode 100644 index 0000000..a16d124 --- /dev/null +++ b/src/LineSprite.js @@ -0,0 +1,76 @@ +export class LineSprite extends PIXI.Sprite { + static textureCache = {}; + static maxWidth = 100; + static maxColors = 100; + static colors = 0; + static canvas = null; + static baseTexture = null; + + constructor(thickness, color, x1, y1, x2, y2) { + super(LineSprite.getTexture(thickness, color)); + this._thickness = thickness; + this._color = color; + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.updatePosition(); + this.anchor = new PIXI.ObservablePoint(() => { return; }, undefined, 0.5); + } + // <-- LineSprite Constructor End --> + + static initCanvas() { + LineSprite.canvas = document.createElement('canvas'); + LineSprite.canvas.width = LineSprite.maxWidth + 2; + LineSprite.canvas.height = LineSprite.maxColors; + LineSprite.baseTexture = new PIXI.BaseTexture(LineSprite.canvas); + } + + static getTexture(thickness, color) { + let key = thickness + '-' + color; + if (!LineSprite.textureCache[key]) { + if (!LineSprite.canvas) { + LineSprite.initCanvas(); + } + let canvas = LineSprite.canvas; + let context = canvas.getContext('2d'); + context.fillStyle = PIXI.utils.hex2string(color); + context.fillRect(1, LineSprite.colors, thickness, 1); + let texture = new PIXI.Texture(LineSprite.baseTexture, PIXI.SCALE_MODES.LINEAR); + texture.frame = new PIXI.Rectangle(0, LineSprite.colors, thickness + 2, 1); + LineSprite.textureCache[key] = texture; + LineSprite.colors++; + } + + return LineSprite.textureCache[key]; + } + + updatePosition() { + this.position.x = this.x1; + this.position.y = this.y1; + this.height = Math.sqrt((this.x2 - this.x1) * (this.x2 - this.x1) + (this.y2 - this.y1) * (this.y2 - this.y1)); + var dir = Math.atan2(this.y1 - this.y2, this.x1 - this.x2); + this.rotation = Math.PI * 0.5 + dir; + } +} + +Object.defineProperties(LineSprite.prototype, { + thickness: { + get: function() { + return this._thickness; + }, + set: function(value) { + this._thickness = value; + this.texture = LineSprite.getTexture(this._thickness, this._color); + }, + }, + color: { + get: function() { + return this._color; + }, + set: function(value) { + this._color = value; + this.texture = LineSprite.getTexture(this._thickness, this._color); + }, + }, +}); diff --git a/scripts_1_6_dev/PAGA_viewer.css b/src/PAGA_viewer.css similarity index 100% rename from scripts_1_6_dev/PAGA_viewer.css rename to src/PAGA_viewer.css diff --git a/src/PAGA_viewer.js b/src/PAGA_viewer.js new file mode 100644 index 0000000..260bccc --- /dev/null +++ b/src/PAGA_viewer.js @@ -0,0 +1,362 @@ +import * as d3 from 'd3'; +import { project_directory } from './main'; + +export default class PAGA { + /** @type PAGA */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call PAGA.create()!'); + } + return this._instance; + } + + static async create() { + if (!this._instance) { + this._instance = new PAGA(); + await this._instance.loadData(); + return this._instance; + } else { + throw new Error('PAGA.create() has already been called, get the existing instance with PAGA.instance!'); + } + } + + constructor() { + this.PAGA_data = { + edge_weight_meta: { + max_edge_weight: 1, + min_edge_weight: 1, + }, + }; + + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'PAGA_popup'); + + this.popup + .append('div') + .style('padding', '5px') + .style('height', '35px') + .append('text') + .text('Graph abstraction') + .attr('id', 'PAGA_title') + .append('input') + .attr('type', 'checkbox') + .attr('checked', true) + .attr('id', 'PAGA_visibility_checkbox') + .style('margin-left', '27px') + .on('click', () => this.toggle_PAGA_visibility()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Node size scale') + .append('input') + .attr('type', 'range') + .attr('value', '40') + .attr('id', 'PAGA_node_size_slider') + .style('margin-left', '29px') + .on('input', () => this.PAGA_redraw()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Edge width scale') + .append('input') + .attr('type', 'range') + .attr('id', 'PAGA_edge_width_slider') + .style('margin-left', '22px') + .attr('value', '40') + .on('input', () => this.PAGA_redraw()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Min edge weight') + .append('input') + .attr('type', 'range') + .attr('value', '25') + .attr('id', 'PAGA_min_edge_weight_slider') + .style('margin-left', '26px') + .on('input', () => this.adjust_min_edge_weight()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Cell mask opacity') + .append('input') + .attr('type', 'range') + .attr('value', '70') + .attr('id', 'PAGA_mask_opacity_slider') + .style('margin-left', '20px') + .on('input', () => this.adjust_PAGA_mask_opacity()); + + this.PAGA_button_options = this.popup + .append('div') + .style('margin-top', '9px') + .style('margin-left', '2px'); + + this.PAGA_button_options.append('button') + .text('Reset') + .on('click', () => this.reset_positions()); + this.PAGA_button_options.append('button') + .text('(De)select') + .on('click', () => this.deselect_PAGA()); + this.PAGA_button_options.append('button') + .text('Propagate') + .on('click', () => this.propagate()); + this.PAGA_button_options.append('button') + .text('Close') + .on('click', () => this.hide_PAGA_popup()); + + this.popup.call( + d3 + .drag() + .on('start', () => this.PAGA_popup_dragstarted()) + .on('drag', () => this.PAGA_popup_dragged()) + .on('end', () => this.PAGA_popup_dragended()), + ); + + this.noCache = new Date().getTime(); + } + // <-- PAGA Constructor End --> + + async loadData() { + try { + const data = await d3.json(project_directory + '/PAGA_data.json' + '?_=' + this.noCache); + if (data !== undefined) { + this.PAGA_data = data; + + let min_weight_frac = data.edge_weight_meta.min_edge_weight / data.edge_weight_meta.max_edge_weight; + d3.select('#PAGA_min_edge_weight_slider').node().value = Math.log(min_weight_frac * Math.exp(100 / 20)) * 20; + + let PAGA_links = d3 + .select('#vis') + .selectAll('line') + .data(data.links) + .enter() + .append('line') + .attr('class', 'PAGA_link'); + + let PAGA_circles = d3 + .select('#vis') + .selectAll('circle') + .data(data.nodes) + .enter() + .append('circle') + .attr('class', 'PAGA_node') + .call( + d3 + .drag() + .on('start', () => this.dragstarted()) + .on('drag', () => this.dragged()) + .on('end', () => this.dragended()), + ) + .on('click', d => { + if (!d3.event.defaultPrevented) { + d.selected = !d.selected; + this.PAGA_redraw(); + } + }); + + this.PAGA_node_dict = {}; + data.nodes.forEach(d => { + this.PAGA_node_dict[d.index] = d; + d.coordinates_original = Object.assign({}, d.coordinates); + }); + + this.PAGA_redraw(); + + PAGA_circles.attr('stroke', 'yellow') + .attr('stroke-width', '0px') + .style('visibility', 'hidden'); + + PAGA_links.style('visibility', 'hidden'); + } + } catch (e) { + console.log(e); + } + } + + dragstarted(d) { + d3.event.sourceEvent.stopPropagation(); + d.beingDragged = true; + if (d.selected === true) { + d3.selectAll('.PAGA_node') + .filter(() => { + return d.selected; + }) + .each(() => { + d.beingDragged = true; + }); + } + } + + dragged(d) { + d3.selectAll('.PAGA_node') + .filter(() => { + return d.beingDragged; + }) + .each(() => { + d.coordinates[0] += d3.event.dx; + d.coordinates[1] += d3.event.dy; + }); + this.PAGA_redraw(); + } + + dragended(d) { + d3.selectAll('.PAGA_node').each(() => { + d.beingDragged = false; + }); + } + + PAGA_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + PAGA_popup_dragged() { + let cx = parseFloat( + d3 + .select('#PAGA_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#PAGA_popup') + .style('top') + .split('px')[0], + ); + d3.select('#PAGA_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#PAGA_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + PAGA_popup_dragended() { + return; + } + + reset_positions() { + d3.selectAll('.PAGA_node').each(d => { + d.coordinates = Object.assign({}, d.coordinates_original); + }); + this.PAGA_redraw(); + } + + propagate() { + return; + } + + deselect_PAGA() { + let any_selected = false; + d3.selectAll('.PAGA_node').each(d => { + if (d.selected) { + any_selected = true; + } + }); + d3.selectAll('.PAGA_node').each(d => { + d.selected = !any_selected; + }); + this.PAGA_redraw(); + } + + adjust_min_edge_weight() { + let min_weight = + Math.exp(parseFloat(d3.select('#PAGA_min_edge_weight_slider').node().value) / 20) / Math.exp(100 / 20); + console.log(min_weight * this.PAGA_data.edge_weight_meta.max_edge_weight); + this.PAGA_data.edge_weight_meta.min_edge_weight = min_weight * this.PAGA_data.edge_weight_meta.max_edge_weight; + this.PAGA_redraw(); + } + + PAGA_redraw() { + let node_scale = (d3.select('#PAGA_node_size_slider').node().value / 40) ** 1.8; + let edge_scale = (d3.select('#PAGA_edge_width_slider').node().value / 40) ** 3; + + d3.selectAll('.PAGA_node') + .attr('cx', d => { + return d.coordinates[0]; + }) + .attr('cy', d => { + return d.coordinates[1]; + }) + .attr('r', d => { + return Math.sqrt(d.size) * 2 * node_scale; + }) + .attr('fill', d => { + return d.color; + }) + .attr('stroke-width', d => { + if (d.selected) { + return (15 + (Math.sqrt(d.size) / 5) * node_scale).toString() + 'px'; + } else { + return '0px'; + } + }); + + d3.selectAll('.PAGA_link') + .attr('opacity', 0.8) + .attr('stroke', 'darkgray') + .attr('x1', d => { + return this.PAGA_node_dict[d.source].coordinates[0]; + }) + .attr('y1', d => { + return this.PAGA_node_dict[d.source].coordinates[1]; + }) + .attr('x2', d => { + return this.PAGA_node_dict[d.target].coordinates[0]; + }) + .attr('y2', d => { + return this.PAGA_node_dict[d.target].coordinates[1]; + }) + .attr('stroke-width', d => { + return d.weight * edge_scale; + }) + .style('visibility', d => { + if (d.weight > this.PAGA_data.edge_weight_meta.min_edge_weight) { + return 'visible'; + } else { + return 'hidden'; + } + }); + } + + adjust_PAGA_mask_opacity() { + let opacity = parseFloat(d3.select('#PAGA_mask_opacity_slider').node().value) / 100; + d3.select('svg').style('background', 'rgba(255,255,255,' + opacity.toString() + ')'); + } + + toggle_PAGA_visibility() { + if (document.getElementById('PAGA_visibility_checkbox').checked) { + d3.selectAll('.PAGA_node').style('visibility', 'visible'); + d3.selectAll('.PAGA_link').style('visibility', 'visible'); + this.adjust_PAGA_mask_opacity(); + this.PAGA_redraw(); + } else { + d3.selectAll('.PAGA_node').style('visibility', 'hidden'); + d3.selectAll('.PAGA_link').style('visibility', 'hidden'); + d3.select('svg').style('background', 'rgba(255,255,255,0'); + } + } + + show_PAGA_popup() { + d3.select('#PAGA_popup').style('visibility', 'visible'); + this.toggle_PAGA_visibility(); + } + + hide_PAGA_popup() { + d3.select('#PAGA_popup').style('visibility', 'hidden'); + } +} diff --git a/scripts_1_6_dev/clone_viewer.css b/src/clone_viewer.css similarity index 100% rename from scripts_1_6_dev/clone_viewer.css rename to src/clone_viewer.css diff --git a/src/clone_viewer.js b/src/clone_viewer.js new file mode 100644 index 0000000..82b6970 --- /dev/null +++ b/src/clone_viewer.js @@ -0,0 +1,647 @@ +import * as d3 from 'd3'; +import * as Spinner from 'spinner'; + +import { SPRITE_IMG_WIDTH, rgbToHex } from './util'; +import { colorBar, forceLayout, project_directory } from './main'; + +export default class CloneViewer { + /** @type CloneViewer */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call CloneViewer.create()!'); + } + return this._instance; + } + + static async create() { + if (!this._instance) { + this._instance = new CloneViewer(); + await this._instance.loadData(); + return this._instance; + } else { + throw new Error( + 'CloneViewer.create() has already been called, get the existing instance with CloneViewer.instance!', + ); + } + } + + constructor() { + this.svg_graph = null; + this.targetCircle = new PIXI.Graphics(); + this.clone_nodes = new Array(); + this.clone_edges = new Array(); + this.node_status = new Array(); + this.clone_sprites = new PIXI.Container(); + this.edge_container = new PIXI.Container(); + this.clone_edge_container = new PIXI.Container(); + + this.clone_edge_container.position = forceLayout.sprites.position; + this.clone_edge_container.scale = forceLayout.sprites.scale; + + this.clone_sprites.position = forceLayout.sprites.position; + this.clone_sprites.scale = forceLayout.sprites.scale; + + this.targetCircle.alpha = 0; + + this.clone_sprites.addChild(this.targetCircle); + + this.show_clone_edges = false; + this.show_source_nodes = false; + + forceLayout.app.stage.addChild(this.clone_edge_container); + forceLayout.app.stage.addChild(this.clone_sprites); + + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'clone_viewer_popup'); + + this.popup + .append('div') + .style('padding', '5px') + .style('height', '22px') + .append('text') + .text('Linkage browser') + .attr('id', 'clone_title'); + + this.clone_key = ''; + + this.cloneKeyMenu = this.popup + .append('div') + .append('select') + .style('font-size', '13px') + // .style('margin-left','50px') + //.style("text-align", "center") + .attr('id', 'clone_key_menu') + .on('change', () => { + this.clone_key = document.getElementById('clone_key_menu').value; + }); + + this.cloneDispatch = d3.dispatch('load', 'statechange'); + this.cloneDispatch.on('load', data => { + this.cloneKeyMenu.selectAll('option').remove(); + this.cloneKeyMenu + .selectAll('option') + .data(Object.keys(data)) + .enter() + .append('option') + .attr('value', d => { + return d; + }) + .text(d => { + return d; + }); + this.cloneDispatch.on('statechange', state => { + select.property('value', state.id); + }); + }); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Selector radius') + .append('input') + .attr('type', 'range') + .attr('id', 'clone_selector_size_slider') + .style('margin-left', '21px') + .attr('value', '25') + .on('input', () => this.draw_target_circle()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Highlight size') + .append('input') + .attr('type', 'range') + .attr('value', '25') + .attr('id', 'clone_node_size_slider') + .style('margin-left', '31px') + .on('input', () => this.update_highlight_size()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Darkness level') + .append('input') + .attr('type', 'range') + .attr('value', '25') + .attr('id', 'clone_darkness_slider') + .style('margin-left', '23px') + .on('input', () => this.darken_nodes()); + + this.popup + .append('div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .append('label') + .text('Max group size') + .append('input') + .attr('type', 'text') + .attr('value', '0') + .attr('id', 'clone_size_input') + .style('margin-left', '23px') + .on('input', () => this.darken_nodes()); + + let source_target_options = this.popup.append('div'); + source_target_options + .append('button') + .text('Source') + .style('width', '60px') + .on('click', () => this.set_source_from_selection()); + source_target_options + .append('button') + .text('Target') + .style('width', '60px') + .on('click', () => this.set_target_from_selection()); + source_target_options + .append('button') + .text('Reset all') + .style('width', '87px') + .on('click', () => this.reset_all_nodes()); + + let node_color_options = this.popup.append('div'); + node_color_options + .append('button') + .text('Darken') + .on('click', () => this.darken_nodes()); + + node_color_options + .append('button') + .text('Burn') + .on('click', () => this.burn()); + + node_color_options + .append('button') + .text('Restore') + .on('click', () => this.restore_colors()); + + node_color_options + .append('button') + .text('Clear') + .on('click', () => this.clear_clone_overlays()); + + let show_things_options = this.popup.append('div'); + show_things_options + .append('button') + .style('width', '106px') + .text('Show edges') + .on('click', d => { + if (this.show_clone_edges) { + this.show_clone_edges = false; + d3.select(d).text('Show edges'); + } else { + this.show_clone_edges = true; + d3.select(d).text('Hide edges'); + } + }); + + show_things_options + .append('button') + .style('width', '106px') + .text('Show source') + .on('click', d => { + if (this.show_source_nodes) { + this.show_source_nodes = false; + d3.select(d).text('Show source'); + } else { + this.show_source_nodes = true; + d3.select(d).text('Hide source'); + } + }); + + let other_options = this.popup.append('div'); + + other_options + .append('button') + .text('Extend from selection') + .style('width', '156px') + .on('click', () => this.extend_from_selection()); + + other_options + .append('button') + .text('Close') + .style('width', '56px') + .on('click', () => this.close_clone_viewer()); + + this.loading_screen = this.popup.append('div').attr('id', 'clone_loading_screen'); + + this.show_waiting_wheel(); + this.loading_screen.append('p').text('Loading linkage data'); + + d3.select('#clone_viewer_popup').call( + d3 + .drag() + .on('start', () => this.clone_viewer_popup_dragstarted()) + .on('drag', () => this.clone_viewer_popup_dragged()) + .on('end', () => this.clone_viewer_popup_dragended()), + ); + + return this; + } + // <-- CloneViewer Constructor End --> + + async loadData() { + this.clone_map = {}; + const noCache = new Date().getTime(); + const filePath = project_directory + '/clone_map.json'; + try { + const cloneData = await d3.json(filePath + '?_=' + noCache); + //console.log(error); + for (let k in cloneData) { + this.clone_map[k] = {}; + for (let i in forceLayout.all_nodes) { + this.clone_map[k][i] = []; + } + for (let i in cloneData[k]) { + this.clone_map[k][i] = cloneData[k][i]; + } + } + } catch (e) { + console.log(`Error getting clone data, continuing.\n${e}`); + } finally { + d3.select('#clone_loading_screen').style('visibility', 'hidden'); + this.cloneDispatch.call('load', this, this.clone_map); + this.clone_key = document.getElementById('clone_key_menu').value; + } + } + + reset() { + return; + } + + show_waiting_wheel() { + this.loading_screen.append('div').attr('id', 'clone_wheel_mask'); + let opts = { + className: 'spinner', // The CSS class to assign to the spinner + color: '#000', // #rgb or #rrggbb or array of colors + corners: 1, // Corner roundness (0..1) + direction: 1, // 1: clockwise, -1: counterclockwise + fps: 20, // Frames per second when using setTimeout() as a fallback for CSS + hwaccel: true, // Whether to use hardware acceleration + left: '50%', // Left position relative to parent + length: 35, // The length of each line + lines: 17, // The number of lines to draw + opacity: 0.2, // Opacity of the lines + position: 'relative', // Element positioning + radius: 50, // The radius of the inner circle + rotate: 8, // The rotation offset + scale: 0.22, // Scales overall size of the spinner + shadow: false, // Whether to render a shadow + speed: 0.9, // Rounds per second + top: '50%', // Top position relative to parent + trail: 60, // Afterglow percentage + width: 15, // The line thickness + zIndex: 2e9, // The z-index (defaults to 2000000000) + }; + let target = document.getElementById('clone_wheel_mask'); + let spinner = new Spinner(opts).spin(target); + $(target).data('spinner', spinner); + return this; + } + + clone_viewer_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + clone_viewer_popup_dragged() { + let cx = parseFloat( + d3 + .select('#clone_viewer_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#clone_viewer_popup') + .style('top') + .split('px')[0], + ); + d3.select('#clone_viewer_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#clone_viewer_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + clone_viewer_popup_dragended() { + return; + } + + reset_all_nodes() { + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + this.node_status[i].source = false; + this.node_status[i].target = false; + } + } + + clear_clone_overlays() { + for (let i in this.clone_nodes) { + this.deactivate_nodes(i); + } + for (let i in this.clone_edges) { + this.deactivate_edges(i); + } + } + + close_clone_viewer() { + this.svg_graph.on('mousemove', null); + this.svg_graph.on('click', null); + this.popup.style('visibility', 'hidden'); + this.reset_all_nodes(); + this.clear_clone_overlays(); + this.targetCircle.clear(); + } + + update_highlight_size() { + let my_scale = parseFloat(d3.select('#clone_node_size_slider').node().value) / 10; + for (let i in this.clone_nodes) { + this.clone_nodes[i].scale.x = forceLayout.all_nodes[i].scale.x * my_scale; + this.clone_nodes[i].scale.y = forceLayout.all_nodes[i].scale.x * my_scale; + } + } + + burn() { + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (this.clone_nodes[i] === undefined && forceLayout.all_nodes[i].tint === '0x000000') { + forceLayout.base_colors[i] = { r: 0, g: 0, b: 0 }; + } + } + colorBar.update_tints(); + forceLayout.app.stage.children.sort((a, b) => { + return ( + colorBar.average_color(forceLayout.base_colors[a.tabIndex]) - + colorBar.average_color(forceLayout.base_colors[b.tabIndex]) + ); + }); + + this.clear_clone_overlays(); + } + + restore_colors() { + colorBar.setNodeColors(); + } + + extend_from_selection() { + for (let i in this.clone_nodes) { + if (!this.clone_nodes[i].active_stable) { + this.deactivate_nodes(i); + } + } + for (let i in this.clone_edges) { + this.deactivate_edges(i); + } + + let maxGroupSize = parseFloat(d3.select('#clone_size_input').node().value); + if (maxGroupSize > 0 === false) { + maxGroupSize = 100000000; + } + + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if ( + forceLayout.all_outlines[i].selected && + this.clone_map[this.clone_key][i].length < maxGroupSize && + this.node_status[i].source + ) { + if (!(i in this.clone_nodes)) { + this.activate_edges(i, false); + if (this.show_source_nodes) { + this.activate_node(i, false); + } + } + } + } + } + + draw_target_circle() { + this.targetCircle.clear(); + this.targetCircle.lineStyle(7, 0xffffff); //(thickness, color) + this.targetCircle.drawCircle(0, 0, this.get_clone_radius() + forceLayout.all_nodes[0].scale.x * SPRITE_IMG_WIDTH); //(x,y,radius) + this.targetCircle.endFill(); + this.targetCircle.alpha = 1; + } + + get_clone_radius() { + let r = parseInt(d3.select('#clone_selector_size_slider').node().value, 10); + return r ** 1.5 / 8; + } + + clone_mousemove() { + let dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = d3.event.clientX - dim.left; + let y = d3.event.clientY - dim.top; + x = (x - forceLayout.sprites.position.x) / forceLayout.sprites.scale.x; + y = (y - forceLayout.sprites.position.y) / forceLayout.sprites.scale.y; + + this.targetCircle.x = x; + this.targetCircle.y = y; + + for (let i in this.clone_nodes) { + if (!this.clone_nodes[i].active_stable) { + this.deactivate_nodes(i); + } + } + + for (let i in this.clone_edges) { + this.deactivate_edges(i); + } + let maxGroupSize = parseFloat(d3.select('#clone_size_input').node().value); + if (maxGroupSize > 0 === false) { + maxGroupSize = 100000000; + } + + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + const rad = Math.sqrt((forceLayout.all_nodes[i].x - x) ** 2 + (forceLayout.all_nodes[i].y - y) ** 2); + if (rad <= this.get_clone_radius()) { + const nodeAndCloneExist = this.node_status[i].source && this.clone_map[this.clone_key] && this.clone_map[this.clone_key][i]; + if (nodeAndCloneExist && this.clone_map[this.clone_key][i].length < maxGroupSize) { + if (!(i in this.clone_nodes)) { + this.activate_edges(i, false); + if (this.show_source_nodes) { + this.activate_node(i, false); + } + } + } + } + } + + this.targetCircle.alpha = 0.75; + setTimeout(() => { + dim_target_circle(0.75); + }, 150); + + const dim_target_circle = newX => { + if (newX > 0 && this.targetCircle.alpha === newX) { + this.targetCircle.alpha = newX - 0.08; + let dimmer = setTimeout(() => { + dim_target_circle(newX - 0.08); + }, 20); + } + }; + } + + start_clone_viewer() { + this.svg_graph = d3.select('#svg_graph'); + + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + this.node_status[i] = { active: false, active_stable: false, source: false, target: false }; + } + this.svg_graph.on('mousemove', () => this.clone_mousemove()); + this.svg_graph.on('click', () => this.clone_click()); + d3.select('#clone_viewer_popup').style('visibility', 'visible'); + + d3.select('#settings_range_background_color').node().value = 65; + forceLayout.app.renderer.backgroundColor = parseInt(rgbToHex(65, 65, 65), 16); + this.draw_target_circle(); + this.set_source_from_selection(); + this.set_target_from_selection(); + this.darken_nodes(); + } + + activate_node(i, stable) { + if (!(i in this.clone_nodes)) { + let circ = PIXI.Sprite.fromImage('stuff/disc.png'); + circ.anchor.set(0.5); + let my_scale = parseFloat(d3.select('#clone_node_size_slider').node().value) / 10; + circ.scale.set(forceLayout.all_nodes[i].scale.x * my_scale); + circ.x = forceLayout.all_nodes[i].x; + circ.y = forceLayout.all_nodes[i].y; + let rgb = forceLayout.base_colors[i]; + circ.tint = parseInt(rgbToHex(rgb.r, rgb.g, rgb.b), 16); + this.node_status[i].active = true; + this.node_status[i].active_stable = stable; + forceLayout.sprites.removeChild(circ); + this.clone_sprites.addChild(circ); + this.clone_nodes[i] = circ; + } + this.clone_nodes[i].active_stable = this.clone_nodes[i].active_stable || stable; + this.clone_nodes[i].x = forceLayout.all_nodes[i].x; + this.clone_nodes[i].y = forceLayout.all_nodes[i].y; + } + + activate_edges(i, stable) { + if (!(i in this.clone_edges)) { + let edge_list = []; + for (let j = 0; j < this.clone_map[this.clone_key][i].length; j++) { + //console.log([i,clone_map[i][j]]); + if (this.node_status[this.clone_map[this.clone_key][i][j]].target) { + this.activate_node(this.clone_map[this.clone_key][i][j], stable); + if (this.show_clone_edges) { + let source = i; + let target = this.clone_map[this.clone_key][i][j]; + let x1 = forceLayout.all_nodes[source].x; + let y1 = forceLayout.all_nodes[source].y; + let x2 = forceLayout.all_nodes[target].x; + let y2 = forceLayout.all_nodes[target].y; + let rgb = forceLayout.base_colors[this.clone_map[this.clone_key][i][j]]; + let color = rgbToHex(rgb.r, rgb.g, rgb.b); + let line = new PIXI.Graphics(); + line.lineStyle(5, parseInt(color, 16), 1); + line.moveTo(x1, y1); + line.lineTo(x2, y2); + this.clone_edge_container.addChild(line); + edge_list.push(line); + } + } + } + this.clone_edges[i] = edge_list; + } else if (stable) { + for (let j = 0; j < this.clone_map[this.clone_key][i].length; j++) { + if (this.node_status[this.clone_map[this.clone_key][i][j]].target) { + this.activate_node(this.clone_map[this.clone_key][i][j], stable); + } + } + } + } + + clone_click() { + /* + let dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = d3.event.clientX - dim.left; + let y = d3.event.clientY - dim.top; + x = (x - sprites.position.x) / sprites.scale.x; + y = (y - sprites.position.y) / sprites.scale.y; + for (let i=0; i { + console.log(data); + let t1 = new Date(); + console.log('Ran clustering: ', t1.getTime() - t0.getTime()); + d3.select('#cluster_popup') + .select('text') + .text('Clustering complete! See Cell Labels menu.'); + this.show_notification(d3.select('#cluster_popup')); + + let noCache = new Date().getTime(); + + d3.json(graph_directory + '/' + sub_directory + '/categorical_coloring_data.json' + '?_=' + noCache).then( + json => { + let categorical_coloring_data = json; + Object.keys(categorical_coloring_data).forEach(k => { + let label_counts = {}; + Object.keys(categorical_coloring_data[k].label_colors).forEach(n => { + label_counts[n] = 0; + }); + categorical_coloring_data[k].label_list.forEach(n => { + label_counts[n] += 1; + }); + categorical_coloring_data[k].label_counts = label_counts; + }); + + colorBar.dispatch.call(categorical_coloring_data, 'cell_labels'); + colorBar.update_slider(); + }, + ); + }, + + type: 'POST', + url: 'cgi-bin/run_clustering.py', + }); + } else { + console.log('no clustering allowed'); + //show_notification(d3.select("#no_clustering_popup")); + d3.select('#cluster_popup') + .select('text') + .text('Sorry, this dataset cannot be edited.'); + this.show_notification(d3.select('#cluster_popup')); + } + } +} diff --git a/scripts_1_6_dev/cluster2_style.css b/src/cluster2_style.css similarity index 100% rename from scripts_1_6_dev/cluster2_style.css rename to src/cluster2_style.css diff --git a/src/cluster_script.js b/src/cluster_script.js new file mode 100755 index 0000000..fe75567 --- /dev/null +++ b/src/cluster_script.js @@ -0,0 +1,688 @@ +import * as d3 from 'd3'; +import * as html2canvas from 'html2canvas'; + +import { colorBar, forceLayout, sub_directory } from './main'; +import { collapse_settings } from './settings_script'; + +export default class Cluster { + /** @type Cluster */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call Cluster.create()!'); + } + return this._instance; + } + + static async create() { + if (!this._instance) { + this._instance = new Cluster(); + await this._instance.loadData(); + return this._instance; + } else { + throw new Error('Cluster.create() has already been called, get the existing instance with Cluster.instance!'); + } + } + + constructor() { + this.spectrum_dropdown = false; + this.explain_dropdown = false; + + this.clustering_data = {}; + this.current_clus_name = ''; + this.last_clus_name = ''; + d3.select('#cluster_dropdown_button').on('click', this.showClusterDropdown); + this.name = sub_directory; + + this.svg_width = parseInt(d3.select('svg').attr('width'), 10); + d3.select('#create_cluster_box').call( + d3 + .drag() + .on('start', this.cluster_box_dragstarted) + .on('drag', this.cluster_box_dragged) + .on('end', this.cluster_box_dragended), + ); + + d3.select('#cluster_view_button').on('click', () => { + this.current_clus_name = 'Cluster' + document.getElementById('enter_cluster_number').value; + let N = parseInt(document.getElementById('enter_cluster_number').value, 10); + let Nmax = Object.keys(this.clustering_data.clusters).length; + if (N > Nmax || N < 1) { + sweetAlert({ + animation: 'slide-from-top', + title: 'The number of clusters must be between 1 and ' + Nmax.toString(), + }); + } else { + this.view_current_clusters(); + } + }); + + d3.select('#cluster_apply_button').on('click', () => { + this.current_clus_name = 'Cluster' + document.getElementById('enter_cluster_number').value; + let N = parseInt(document.getElementById('enter_cluster_number').value, 10); + let Nmax = Object.keys(this.clustering_data.clusters).length; + if (N > Nmax || N < 1) { + alert('The number of clusters must be between 1 and ' + Nmax.toString()); + } else { + this.view_current_clusters(); + if (d3.select('#update_cluster_labels_box').style('visibility') === 'visible') { + this.show_update_cluster_labels_box(); + } + this.last_clus_name = this.current_clus_name; + } + this.save_cluster_data(); + }); + + d3.select('#cluster_close_button').on('click', () => { + this.current_clus_name = this.last_clus_name; + colorBar.categorical_coloring_data['Current clustering'] = this.clustering_data.clusters[this.current_clus_name]; + if (document.getElementById('labels_button').checked) { + this.view_current_clusters(); + } + if (d3.select('#update_cluster_labels_box').style('visibility') === 'visible') { + this.show_update_cluster_labels_box(); + } + this.hide_create_cluster_box(); + colorBar.categorical_coloring_data['Current clustering'] = this.clustering_data.clusters[this.current_clus_name]; + }); + + d3.select('#enter_cluster_number').on('mousedown', () => { + d3.event.stopPropagation(); + }); + + d3.select('#cluster_help_choose_button').on('click', () => { + this.toggle_spectrum(); + }); + + d3.select('#cluster_explanation_button').on('click', () => { + this.toggle_explain(); + }); + + d3.select('#update_cluster_labels_box').call( + d3 + .drag() + .on('start', () => this.update_cluster_labels_box_dragstarted()) + .on('drag', () => this.update_cluster_labels_box_dragged()) + .on('end', () => this.update_cluster_labels_box_dragended()), + ); + } + // <-- Cluster Constructor End --> + + async loadData() { + try { + this.clustering_data = await d3.json('data/clustering_data/' + name + '_clustering_data.json'); + this.current_clus_name = this.clustering_data.Current_clustering; + this.last_clus_name = this.current_clus_name; + } catch (e) { + console.log(e); + } + } + + cluster_box_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + cluster_box_dragged() { + let cx = parseFloat( + d3 + .select('#create_cluster_box') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#create_cluster_box') + .style('top') + .split('px')[0], + ); + d3.select('#create_cluster_box').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#create_cluster_box').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + cluster_box_dragended() { + return; + } + + update_cluster_labels_box_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + update_cluster_labels_box_dragged() { + let cx = parseFloat( + d3 + .select('#update_cluster_labels_box') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#update_cluster_labels_box') + .style('top') + .split('px')[0], + ); + d3.select('#update_cluster_labels_box').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#update_cluster_labels_box').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + update_cluster_labels_box_dragended() { + return; + } + + hide_create_cluster_box = () => { + d3.select('#create_cluster_box').style('visibility', 'hidden'); + }; + + show_create_cluster_box = () => { + let mywidth = parseInt( + d3 + .select('#create_cluster_box') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#create_cluster_box') + .style('visibility', 'visible') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '146px'); + document.getElementById('enter_cluster_number').value = this.current_clus_name.split('Cluster')[1]; + }; + + showClusterDropdown() { + if (d3.select('#cluster_dropdown').style('height') === 'auto') { + forceLayout.closeDropdown(); + collapse_settings(); + setTimeout(() => { + document.getElementById('cluster_dropdown').classList.toggle('show'); + }, 10); + } + } + + view_current_clusters() { + colorBar.categorical_coloring_data['Current clustering'] = this.clustering_data.clusters[this.current_clus_name]; + if (d3.selectAll('#cluster_option')[0].length === 0) { + d3.select('#labels_menu') + .append('option') + .attr('value', 'Current clustering') + .attr('id', 'cluster_option') + .text('Current_clustering'); + } + d3.select('#labels_menu').property('value', 'Current clustering'); + + document.getElementById('channels_button').checked = false; + document.getElementById('gradient_button').checked = false; + document.getElementById('labels_button').checked = true; + let cat_color_map = this.clustering_data.clusters[this.current_clus_name].label_colors; + let cat_label_list = this.clustering_data.clusters[this.current_clus_name].label_list; + d3.select('#label_column') + .selectAll('div') + .remove(); + d3.select('#count_column') + .selectAll('div') + .remove(); + d3.select('#legend_mask') + .transition() + .attr('x', this.svg_width - 170) + .each(() => { + colorBar.make_legend(cat_color_map, cat_label_list); + this.color_nodes(cat_color_map, cat_label_list); + }); + } + + color_nodes(cat_color_map, cat_label_list) { + d3.select('.node') + .selectAll('circle') + .style('fill', d => { + return cat_color_map[cat_label_list[d.number]]; + }); + } + + make_new_clustering() { + this.show_create_cluster_box(); + } + + toggle_spectrum() { + if (this.explain_dropdown === true) { + this.hide_explain(); + setTimeout(this.show_spectrum, 400); + } else { + if (this.spectrum_dropdown === true) { + this.hide_spectrum(); + } else { + this.show_spectrum(); + } + } + } + + show_spectrum() { + console.log('showspec'); + // Set the dimensions of the canvas / graph + const margin = { top: 30, right: 20, bottom: 55, left: 75 }; + const width = 648 - margin.left - margin.right; + const height = 280 - margin.top - margin.bottom; + + // Set the ranges + let x = d3.scaleLinear().range([0, width]); + let y = d3.scaleLinear().range([height, 0]); + + // Define the axes + let xAxis = d3.axisBottom(x).ticks(10); + + let yAxis = d3.axisLeft(y).ticks(5); + + // Define the line + let valueline = d3 + .line() + .x(d => { + return x(d[0]); + }) + .y(d => { + return y(d[1]); + }); + + let argmax_line = d3 + .line() + .x(d => { + return x(d[0]); + }) + .y(d => { + return y(d[1]); + }); + + // Adds the svg canvas + let svg = d3 + .select('#cluster_plot_window') + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .style('background-color', 'rgba(255,255,255,.82)') + .style('margin-left', '30px') + .style('margin-right', '30px') + .style('margin-top', '10px') + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + // Get the data + this.gap_data = []; + this.clustering_data.spectral_info.gaps.forEach((val, i) => { + let dd = Object(); + dd.y = val; + dd.x = i + 1; + this.gap_data.push(dd); + }); + + // Scale the range of the data + let maxval = d3.max(this.gap_data, d => { + return d.y; + }); + x.domain( + d3.extent(this.gap_data, d => { + return d.x; + }), + ); + y.domain([0, maxval]); + + let argmax = this.clustering_data.spectral_info.argmax; + svg + .append('line') + .attr('x1', x(argmax)) + .attr('y1', y(0)) + .attr('x2', x(argmax)) + .attr('y2', y(maxval)) + .attr('stroke', 'rgba(100,100,100,1)') + .style('stroke-dasharray', 5) + .style('stroke-width', '2px'); + + // Add the valueline path. + svg + .append('path') + .attr('class', 'line') + .attr('d', valueline(this.gap_data)); + + svg + .append('path') + .attr('class', 'dotted-line') + .attr('d', valueline(this.gap_data)); + + // Add the X Axis + svg + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis); + + // Add the Y Axis + svg + .append('g') + .attr('class', 'y axis') + .call(yAxis); + + svg + .append('text') + .attr('transform', 'rotate(-90)') + .attr('y', 0 - margin.left + 8) + .attr('x', 0 - height / 2) + .attr('dy', '1em') + .style('text-anchor', 'middle') + .text('Spectral gap'); + + svg + .append('text') + .attr('transform', 'translate(' + width / 2 + ' ,' + (height + margin.top + 12) + ')') + .style('text-anchor', 'middle') + .text('Number of clusters'); + + svg.selectAll('text').style('font', '14px sans-serif'); + + let mywidth = + parseInt( + d3 + .select('#create_cluster_box') + .style('width') + .split('px')[0], + 10, + ) - 54; + d3.select('#cluster_text_window') + .style('margin-left', '30px') + .style('margin-top', '10px') + .style('width', mywidth.toString() + 'px'); + + let clusnum = this.clustering_data.spectral_info.argmax.toString(); + svg + .append('text') + .attr('transform', 'translate(' + (width - 105) + ' , 5)') + .style('text-anchor', 'middle') + .text('Suggested cluster number = ' + clusnum); + + d3.select('#cluster_text_window') + .append('text') + .style('font', '14px sans-serif') + .style('color', 'white') + .text( + 'The recommended number of clusters is ' + + clusnum + + ', because this is the first' + + ' peak in the spectral gap. Spectral gap measures the difference in ' + + 'how much structure is captured by the current cluster number versus one ' + + 'additional cluster. So when the gap is large, that means not much is gained ' + + 'by further increasing the cluster number. This is similar to "elbow" methods ' + + 'for other other clustering approaches. For for information, click "Explanation' + + ' of method" above.', + ); + + d3.select('#create_cluster_box') + .transition() + .duration(400) + .style('height', '470px'); + + this.spectrum_dropdown = true; + } + + hide_spectrum() { + d3.select('#create_cluster_box') + .transition() + .duration(400) + .style('height', '52px') + .each(() => { + d3.select('#cluster_plot_window') + .select('svg') + .remove(); + d3.select('#cluster_text_window') + .select('text') + .remove(); + }); + this.spectrum_dropdown = false; + } + + toggle_explain() { + if (this.spectrum_dropdown === true) { + this.hide_spectrum(); + setTimeout(this.show_explain, 400); + } else { + if (this.explain_dropdown === true) { + this.hide_explain(); + } else { + this.show_explain(); + } + } + } + + show_explain() { + d3.select('#create_cluster_box') + .transition() + .duration(400) + .style('height', '260px'); + + let mywidth = + parseInt( + d3 + .select('#create_cluster_box') + .style('width') + .split('px')[0], + 10, + ) - 54; + d3.select('#cluster_text_window') + .style('margin-left', '30px') + .style('margin-top', '15px') + .style('width', mywidth.toString() + 'px'); + + d3.select('#cluster_text_window') + .append('text') + .style('font', '14px sans-serif') + .style('color', 'white') + .text( + 'Cells have been clustered using spectral clustering on the SPRING ' + + 'k-nearest-neighbor graph. Spectral clustering a technique where each cell ' + + 'is mapped to new "spectral" coordinates (based on its "position" in the graph' + + ' and then a conventional clustering method (in our case, k-means) is applied' + + ' in these new coordinates. For information, see the links below. There are' + + ' are several letiants of spectral remapping distinguished by the method of' + + ' normalization applied to the graph Laplacian. Here, we are using the "random-walk"' + + ' normalization.', + ); + + d3.select('#cluster_text_window') + .append('div') + .attr('class', 'explain_link') + .style('margin-top', '15px') + .append('a') + .attr('target', '_blank') + .style('color', 'white') + .attr('href', 'https://en.wikipedia.org/wiki/Spectral_clustering') + .append('text') + .text('Wikipedia article on spectral clustering') + .style('font', '14px sans-serif'); + + d3.select('#cluster_text_window') + .append('div') + .attr('class', 'explain_link') + .style('margin-top', '8px') + .append('a') + .attr('target', '_blank') + .style('color', 'white') + .attr('href', 'http://www.cs.cmu.edu/~aarti/Class/10701/readings/Luxburg06_TR.pdf') + .append('text') + .text('A tutorial on spectral clustering (Luxburg, 2006)') + .style('font', '14px sans-serif'); + this.explain_dropdown = true; + } + + hide_explain() { + d3.select('#create_cluster_box') + .transition() + .duration(400) + .style('height', '52px') + .each(() => { + d3.selectAll('.explain_link').remove(); + d3.select('#cluster_text_window') + .select('text') + .remove(); + }); + this.explain_dropdown = false; + } + + show_update_cluster_labels_box() { + if (d3.select('#update_cluster_labels_box').style('visibility') === 'hidden') { + d3.select('#download_legend_button').on('click', this.download_legend_image); + d3.select('#apply_legend_button').on('click', this.apply_legend); + d3.select('#close_cluster_label_button').on('click', this.hide_update_cluster_labels_box); + + let mywidth = parseInt( + d3 + .select('#update_cluster_labels_box') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#update_cluster_labels_box') + .style('visibility', 'visible') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '220px'); + } + + d3.select('#update_cluster_labels_box') + .selectAll('g') + .remove(); + d3.select('#update_cluster_labels_box') + .selectAll('g') + .data(Object.keys(this.clustering_data.clusters[this.current_clus_name].label_colors)) + .enter() + .append('g') + .append('div') + .attr('class', 'cluster_label_row'); + + d3.selectAll('.cluster_label_row').each(function(d) { + d3.select(this) + .append('div') + .attr('class', 'cluster_swatch') + .style('background-color', this.clustering_data.clusters[this.current_clus_name].label_colors[d]); + d3.select(this) + .append('div') + .style('margin-left', '35px') + .style('margin-top', '8px') + .attr('pointer-events', 'none') + .append('form') + .attr('onSubmit', 'return false') + .append('input') + .attr('class', 'cluster_name_input') + .attr('type', 'text') + .attr('value', d) + .on('mousedown', () => { + d3.event.stopPropagation(); + }); + }); + } + + hide_update_cluster_labels_box() { + d3.select('#update_cluster_labels_box').style('visibility', 'hidden'); + } + + apply_legend() { + this.new_clus = Object(); + this.new_colors = Object(); + this.new_labels = []; + this.new_names = []; + this.mapping = Object(); + this.new_name_set = new Set(); + + d3.selectAll('.cluster_label_row').each(d => { + let newname = d3 + .select(d) + .selectAll('.cluster_name_input') + .node().value; + this.new_names.push(newname); + this.new_name_set.add(newname); + this.mapping[d] = newname; + this.new_colors[newname] = this.clustering_data.clusters[this.current_clus_name].label_colors[d]; + }); + + if (this.new_name_set.size < this.new_names.length) { + sweetAlert({ title: 'Cluster names must be distinct', animation: 'slide-from-top' }); + } else { + this.clustering_data.clusters[this.current_clus_name].label_list.forEach(d => { + this.new_labels.push(this.mapping[d]); + }); + + this.new_clus.label_list = this.new_labels; + this.new_clus.label_colors = this.new_colors; + this.clustering_data.clusters[this.current_clus_name] = this.new_clus; + + if (document.getElementById('labels_button').checked) { + if (document.getElementById('labels_menu').value === 'Current clustering') { + d3.select('#label_column') + .selectAll('div') + .remove(); + d3.select('#count_column') + .selectAll('div') + .remove(); + colorBar.make_legend(this.new_colors, this.new_labels); + } + } + this.show_update_cluster_labels_box(); + this.save_cluster_data(); + } + } + + save_cluster_data() { + this.clustering_data.Current_clustering = this.current_clus_name; + const path = 'clustering_data/' + this.name + '_clustering_data_clustmp.json'; + $.ajax({ + data: { path: path, content: JSON.stringify(this.clustering_data, null, ' ') }, + type: 'POST', + url: 'cgi-bin/save_data.py', + }); + } + + download_legend_image() { + const original_visibility = d3.select('#update_cluster_labels_box').style('visibility'); + this.show_update_cluster_labels_box(); + d3.select('#cluster_label_button_bar').style('visibility', 'hidden'); + d3.select('#cluster_label_button_bar').style('height', '5px'); + d3.select('#update_cluster_labels_box').style('background-color', 'white'); + d3.selectAll('.cluster_name_input').style('color', 'black'); + + html2canvas(document.getElementById('update_cluster_labels_box')).then(canvas => { + const a = document.createElement('a'); + // toDataURL defaults to png, so we need to request a jpeg, then convert for file download. + a.href = canvas.toDataURL('image/png'); + a.download = 'SPRING_legend.png'; + a.click(); + }); + + d3.select('#cluster_label_button_bar').style('visibility', 'inherit'); + d3.select('#cluster_label_button_bar').style('height', '31.1px'); + d3.select('#update_cluster_labels_box').style('background-color', 'rgba(80, 80, 80, 0.5)'); + d3.selectAll('.cluster_name_input').style('color', 'white'); + if (original_visibility === 'hidden') { + this.hide_update_cluster_labels_box(); + } + } + + download_clustering() { + let text = ''; + let label_list = this.clustering_data.clusters[this.current_clus_name].label_list; + d3.select('.node') + .selectAll('circle') + .sort((a, b) => { + return a.number - b.number; + }) + .each(d => { + text = text + d.name.toString() + ',' + label_list[d.number] + '\n'; + }); + colorBar.downloadFile(text, 'clustering.txt'); + } +} diff --git a/scripts_1_5_dev/cluster_style.css b/src/cluster_style.css similarity index 100% rename from scripts_1_5_dev/cluster_style.css rename to src/cluster_style.css diff --git a/src/coexpression_script.js b/src/coexpression_script.js new file mode 100755 index 0000000..e3c5475 --- /dev/null +++ b/src/coexpression_script.js @@ -0,0 +1,575 @@ +import * as d3 from 'd3'; +import * as Spinner from 'spinner'; + +import { read_csv } from './util'; + +export const coexpression_setup = project_directory => { + let scatter_jitter = 5; + let scatter_zoom = 1; + let scatter_size = 6; + + let svg_width = parseInt(d3.select('svg').attr('width'), 10); + let c10 = d3.schemeCategory10; + + let scatter_data = null; + let scatter_x = null; + let scatter_y = null; + let scatter_xAxis = null; + let scatter_yAxis = null; + + d3.select('#coexpression_panel').attr('class', 'bottom_tab'); + d3.select('#coexpression_panel').style('visibility', 'visible'); + d3.select('#coexpression_header') + .append('div') + .attr('id', 'coexpression_closeopen') + .append('button') + .text('Open') + .on('click', function() { + if (d3.select('#coexpression_panel').style('height') === '50px') { + open_coexpression(); + } else { + close_coexpression(); + } + }); + + d3.select('#coexpression_header') + .append('div') + .attr('id', 'coexpression_title') + .append('text') + .text('Coexpression browser'); + + d3.select('#coexpression_header') + .append('div') + .attr('id', 'coexpression_refresh_button') + .append('button') + .text('Refresh cluster selection') + .on('click', refresh_selected_clusters); + + d3.select('#coexpression_size_slider').on('input', function() { + scatter_size = this.value / 10; + quick_scatter_update(); + }); + + d3.select('#coexpression_jitter_slider').on('input', function() { + scatter_jitter = parseFloat(this.value); + quick_scatter_update(); + }); + + d3.select('#coexpression_zoom_slider').on('input', function() { + scatter_zoom = (100 - this.value) / 100; + quick_scatter_update(); + }); + + function refresh_selected_clusters() { + d3.select('#coexpression_infobox') + .select('text') + .remove(); + d3.select('#coexpression_panel') + .selectAll('svg') + .remove(); + d3.select('#menuDiv').remove(); + d3.select('#n_cells').remove(); + + make_coexpression_spinner('coexpression_infobox'); + d3.select('#coexpression_infobox') + .append('text') + .text('Loading gene expression data'); + + const list = []; + const expression_dict = {}; + const lengths_list = []; + d3.selectAll('.coexpression_legend_row').remove(); + + d3.selectAll('.selected').each(function(d) { + list.push(d.name); + }); + make_coexpression_legend(list); + load_clusters(list, lengths_list, expression_dict); + } + + function load_clusters(list, lengths_list, expression_dict) { + if (list.length > 0) { + load_cluster_expression(list, lengths_list, expression_dict); + } else { + $('.coexpression_spinner').remove(); + d3.select('#coexpression_infobox') + .select('text') + .remove(); + if (Object.keys(expression_dict).length > 0) { + scatter_setup(lengths_list, expression_dict); + } else { + d3.select('#coexpression_infobox') + .append('text') + .text('No clusters selected'); + } + } + } + + function close_coexpression() { + d3.select('#coexpression_closeopen') + .select('button') + .text('Open'); + d3.select('#coexpression_refresh_button').style('visibility', 'hidden'); + d3.select('#coexpression_panel') + .transition() + .duration(500) + .style('height', '50px') + .style('width', '280px'); + d3.select('#menuDiv').style('visibility', 'hidden'); + d3.select('#n_cells').style('visibility', 'hidden'); + d3.select('#coexpression_panel') + .selectAll('svg') + .style('visibility', 'hidden'); + d3.select('#coexpression_legend').style('visibility', 'hidden'); + d3.select('#coexpression_settings_box').style('visibility', 'hidden'); + } + + function open_coexpression() { + d3.selectAll('#coexpression_legend').style('visibility', 'visible'); + d3.selectAll('#coexpression_settings_box').style('visibility', 'visible'); + setTimeout(function() { + d3.select('#coexpression_refresh_button').style('visibility', 'visible'); + if (d3.selectAll('.coexpression_legend_row')[0].length === 0) { + refresh_selected_clusters(); + } + d3.select('#menuDiv').style('visibility', 'visible'); + d3.select('#n_cells').style('visibility', 'visible'); + d3.select('#coexpression_panel') + .selectAll('svg') + .style('visibility', 'visible'); + }, 500); + d3.select('#coexpression_closeopen') + .select('button') + .text('Close'); + d3.select('#coexpression_panel') + .transition() + .duration(500) + .style('height', '380px') + .style('width', '900px'); + } + + function load_cluster_expression(list, lengths_list, expression_dict) { + let name = list[0]; + list = list.slice(1, list.length); + d3.text(project_directory + '/cluster_expression/' + name + '.csv').then(text => { + let tmp_expression_dict = read_csv(text); + let random_key = Object.keys(tmp_expression_dict)[0]; + lengths_list.push(tmp_expression_dict[random_key].length); + if (Object.keys(expression_dict).length === 0) { + expression_dict = tmp_expression_dict; + } else { + Object.keys(tmp_expression_dict).forEach(function(d) { + expression_dict[d] = expression_dict[d].concat(tmp_expression_dict[d]); + }); + } + load_clusters(list, lengths_list, expression_dict); + }); + } + + function make_coexpression_legend(list) { + d3.select('#coexpression_legend') + .selectAll('.coexpression_legend_row') + .data(list) + .enter() + .append('div') + .style('display', 'inline-block') + .attr('class', 'coexpression_legend_row') + .style('height', '25px') + .style('margin-top', '0px') + .style('overflow', 'scroll'); + + d3.selectAll('.coexpression_legend_row').each(function(d, i) { + d3.select(this) + .append('div') + .style('background-color', c10[i]); + d3.select(this) + .append('div') + .attr('class', 'coexpression_text_label_div') + .append('p') + .text(d) + .style('float', 'left') + .style('white-space', 'nowrap') + .style('margin-top', '-6px') + .style('margin-left', '3px'); + }); + } + + function scatter_setup(lengths_list, expression_dict) { + let menuDiv = d3 + .select('#coexpression_header') + .append('div') + .attr('id', 'menuDiv'); + + menuDiv + .append('text') + .text('X: ') + .attr('class', 'coexp_menu_label'); + let Xmenu = menuDiv + .append('select') + .attr('id', 'Xmenu') + .style('margin-left', '2px') + .style('font-size', '13px') + .style('background-color', '#e4e4e4') + .on('change', function() { + scatter_update(expression_dict); + }) + .sort(function(a, b) { + if (a.text > b.text) { + return 1; + } else if (a.text < b.text) { + return -1; + } else { + return 0; + } + }) + .selectAll('option') + .data(Object.keys(expression_dict)) + .enter() + .append('option') + .attr('value', function(d) { + return d; + }) + .text(function(d) { + return d; + }); + + menuDiv + .append('text') + .text('Y: ') + .attr('class', 'coexp_menu_label'); + let Ymenu = menuDiv + .append('select') + .attr('id', 'Ymenu') + .style('margin-left', '2px') + .style('font-size', '13px') + .style('background-color', '#e4e4e4') + .on('change', function() { + scatter_update(expression_dict); + }) + .sort(function(a, b) { + if (a.text > b.text) { + return 1; + } else if (a.text < b.text) { + return -1; + } else { + return 0; + } + }) + .selectAll('option') + .data(Object.keys(expression_dict)) + .enter() + .append('option') + .attr('value', function(d) { + return d; + }) + .text(function(d) { + return d; + }); + + let geneX = document.getElementById('Xmenu').value; + let geneY = document.getElementById('Ymenu').value; + let xx = expression_dict[geneX]; + let yy = expression_dict[geneY]; + scatter_data = []; + for (let i = 0; i < xx.length; i++) { + scatter_data.push([ + xx[i], + yy[i], + ((Math.random() - 0.3) * d3.max(xx)) / 100, + ((Math.random() - 0.3) * d3.max(yy)) / 100, + ]); + } + + const margin = { top: 15, right: 535, bottom: 130, left: 85 }; + const width = document.getElementById('coexpression_panel').offsetWidth - margin.left - margin.right; + const height = document.getElementById('coexpression_panel').offsetHeight - margin.top - margin.bottom; + + scatter_x = d3 + .scaleLinear() + .domain([ + 0, + d3.max(scatter_data, function(d) { + return d[0]; + }) + 0.1, + ]) + .range([0, width]); + + scatter_y = d3 + .scaleLinear() + .domain([ + 0, + d3.max(scatter_data, function(d) { + return d[1]; + }) + 0.1, + ]) + .range([height, 0]); + + let chart = d3 + .select('#coexpression_panel') + .append('svg:svg') + .attr('width', width + 108) + .attr('height', height + margin.top + margin.bottom) + .attr('class', 'chart'); + + let main = chart + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + .attr('width', width) + .attr('height', height) + .attr('class', 'main'); + + // draw the x axis + scatter_xAxis = d3.axisBottom(scatter_x); + + main + .append('g') + .attr('transform', 'translate(0,' + height + ')') + .attr('class', 'scatter_axis') + .attr('id', 'coexpression_scatter_x_axis') + .call(scatter_xAxis) + .append('text') + .attr('class', 'axis_label') + .attr('fill', '#000') + .attr('y', margin.bottom) + .attr('x', width / 2) + .attr('font-size', '320px') + .attr('dy', '-5.5em') + .style('text-anchor', 'middle') + .text(geneX + ' (UMIs)'); + + // draw the y axis + scatter_yAxis = d3.axisLeft(scatter_y); + + main + .append('g') + .attr('transform', 'translate(0,0)') + .attr('class', 'scatter_axis') + .attr('id', 'coexpression_scatter_y_axis') + .call(scatter_yAxis) + .append('text') + .attr('class', 'axis_label') + .attr('fill', '#000') + .attr('transform', 'rotate(-90)') + .attr('y', -margin.left) + .attr('x', -height / 2) + .attr('font-size', '14px') + .attr('dy', '2.6em') + .style('text-anchor', 'middle') + .text(geneY + ' (UMIs)'); + + let g = main.append('svg:g'); + let current_color_index = 0; + let next_length = lengths_list[0]; + let color_counter = 0; + lengths_list = lengths_list.slice(1, lengths_list.length); + g.selectAll('coexpression-scatter-dots') + .data(scatter_data) + .enter() + .append('svg:circle') + .attr('class', 'coexpression-scatter-dots') + .attr('cx', function(d, i) { + return scatter_x(d[0] + d[2] * scatter_jitter); + }) + .attr('cy', function(d) { + return scatter_y(d[1] + d[3] * scatter_jitter); + }) + .attr('r', 5); + + d3.selectAll('.coexpression-scatter-dots').style('fill', function(d, i) { + if (color_counter > next_length) { + next_length = lengths_list[0]; + lengths_list = lengths_list.slice(1, lengths_list.length); + current_color_index += 1; + color_counter = 0; + } + color_counter += 1; + return c10[current_color_index]; + }); + + geneX = document.getElementById('Xmenu').value; + geneY = document.getElementById('Ymenu').value; + xx = expression_dict[geneX]; + yy = expression_dict[geneY]; + let r = pearsonCorrelation(xx, yy) + .toString() + .slice(0, 3); + + let random_key = Object.keys(expression_dict)[0]; + let n = expression_dict[random_key].length.toString(); + + d3.select('#coexpression_header') + .append('text') + .attr('id', 'n_cells') + .text('n = ' + n + ' cells, r = ' + r); + } + + function scatter_update(expression_dict) { + let duration = 750; + let geneX = document.getElementById('Xmenu').value; + let geneY = document.getElementById('Ymenu').value; + let xx = expression_dict[geneX]; + let yy = expression_dict[geneY]; + scatter_data = []; + for (let i = 0; i < xx.length; i++) { + scatter_data.push([ + xx[i], + yy[i], + ((Math.random() - 0.3) * d3.max(xx)) / 100, + ((Math.random() - 0.3) * d3.max(yy)) / 100, + ]); + } + + scatter_x.domain([ + 0, + d3.max(scatter_data, function(d) { + return d[0]; + }) * + scatter_zoom + + 0.1, + ]); + scatter_y.domain([ + 0, + d3.max(scatter_data, function(d) { + return d[1]; + }) * + scatter_zoom + + 0.1, + ]); + let svg_use = d3.select('#coexpression_panel').transition(); + + d3.selectAll('.coexpression-scatter-dots') + .data(scatter_data) + .enter() + .append('svg:circle'); + + svg_use + .selectAll('.coexpression-scatter-dots') + .duration(duration) + .attr('cx', function(d, i) { + return scatter_x(d[0] + d[2] * scatter_jitter); + }) + .attr('cy', function(d) { + return scatter_y(d[1] + d[3] * scatter_jitter); + }); + + svg_use + .select('#coexpression_scatter_x_axis') + .duration(duration) + .call(scatter_xAxis); + svg_use + .select('#coexpression_scatter_y_axis') + .duration(duration) + .call(scatter_yAxis); + + d3.select('#coexpression_scatter_x_axis .axis_label').text(geneX + ' (UMIs)'); + d3.select('#coexpression_scatter_y_axis .axis_label').text(geneY + ' (UMIs)'); + + let r = pearsonCorrelation(xx, yy) + .toString() + .slice(0, 5); + let random_key = Object.keys(expression_dict)[0]; + let n = expression_dict[random_key].length.toString(); + d3.select('#n_cells').text('n = ' + n + ' cells, r = ' + r); + } + + function quick_scatter_update() { + scatter_x.domain([ + 0, + d3.max(scatter_data, function(d) { + return d[0] * scatter_zoom; + }) + 0.1, + ]); + scatter_y.domain([ + 0, + d3.max(scatter_data, function(d) { + return d[1] * scatter_zoom; + }) + 0.1, + ]); + d3.select('#coexpression_scatter_x_axis').call(scatter_xAxis); + d3.select('#coexpression_scatter_y_axis').call(scatter_yAxis); + + d3.selectAll('.coexpression-scatter-dots') + .data(scatter_data) + .enter() + .append('svg:circle'); + d3.selectAll('.coexpression-scatter-dots') + .attr('cx', function(d, i) { + return scatter_x(d[0] + d[2] * scatter_jitter); + }) + .attr('cy', function(d) { + return scatter_y(d[1] + d[3] * scatter_jitter); + }) + .style('r', scatter_size / (scatter_zoom + 0.1)); + } +}; + +export const make_coexpression_spinner = element => { + let opts = { + className: 'coexpression_spinner', // The CSS class to assign to the spinner + color: 'gray', // #rgb or #rrggbb or array of colors + corners: 1, // Corner roundness (0..1) + direction: 1, // 1: clockwise, -1: counterclockwise + fps: 20, // Frames per second when using setTimeout() as a fallback for CSS + hwaccel: true, // Whether to use hardware acceleration + left: '50%', // Left position relative to parent + length: 50, // The length of each line + lines: 17, // The number of lines to draw + opacity: 0.15, // Opacity of the lines + position: 'absolute', // Element positioning + radius: 60, // The radius of the inner circle + rotate: 8, // The rotation offset + scale: 0.22, // Scales overall size of the spinner + shadow: true, // Whether to render a shadow + speed: 0.9, // Rounds per second + top: '30%', // Top position relative to parent + trail: 60, // Afterglow percentage + width: 20, // The line thickness + zIndex: 2e9, // The z-index (defaults to 2000000000) + }; + let target = document.getElementById(element); + let spinner = new Spinner(opts).spin(target); + $(target).data('spinner', spinner); +}; + +export const pearsonCorrelation = (xx, yy) => { + let prefs = new Array(xx, yy); + let p1 = 0; + let p2 = 1; + + let si = []; + for (let key in prefs[p1]) { + if (prefs[p2][key]) { + si.push(key); + } + } + let n = si.length; + if (n === 0) { + return 0; + } + let sum1 = 0; + for (let i = 0; i < si.length; i++) { + sum1 += prefs[p1][si[i]]; + } + let sum2 = 0; + for (let i = 0; i < si.length; i++) { + sum2 += prefs[p2][si[i]]; + } + let sum1Sq = 0; + for (let i = 0; i < si.length; i++) { + sum1Sq += Math.pow(prefs[p1][si[i]], 2); + } + let sum2Sq = 0; + for (let i = 0; i < si.length; i++) { + sum2Sq += Math.pow(prefs[p2][si[i]], 2); + } + let pSum = 0; + for (let i = 0; i < si.length; i++) { + pSum += prefs[p1][si[i]] * prefs[p2][si[i]]; + } + let num = pSum - (sum1 * sum2) / n; + let den = Math.sqrt((sum1Sq - Math.pow(sum1, 2) / n) * (sum2Sq - Math.pow(sum2, 2) / n)); + if (den === 0) { + return 0; + } + return num / den; +}; diff --git a/scripts_1_5_dev/coexpression_style.css b/src/coexpression_style.css similarity index 100% rename from scripts_1_5_dev/coexpression_style.css rename to src/coexpression_style.css diff --git a/src/colorBar.js b/src/colorBar.js new file mode 100755 index 0000000..7bbc164 --- /dev/null +++ b/src/colorBar.js @@ -0,0 +1,1770 @@ +import * as d3 from 'd3'; +import { show_colorpicker_popup } from './colorpicker_layout'; +import { forceLayout, graph_directory, selectionScript, project_directory } from './main'; +import { rgbToHex, postSelectedCellUpdate, downloadFile } from './util'; + +export default class ColorBar { + /** @type ColorBar */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call ColorBar.create()!'); + } + return this._instance; + } + + static async create(color_menu_genes) { + if (!this._instance) { + this._instance = new ColorBar(color_menu_genes); + + await this._instance.loadData(); + + return this._instance; + } else { + throw new Error('ColorBar.create() has already been called, get the existing instance with ColorBar.instance!'); + } + } + + constructor(color_menu_genes) { + this.color_menu_genes = color_menu_genes; + this.color_profiles = {}; + this.color_option = 'gradient'; + this.noCache = new Date().getTime(); + this.color_max = 1; + this.color_stats = null; + + /* ----------------------------------------------------------------------------------- + Top menu bar + */ + this.menuBar = d3.select('#color_chooser'); + // const enrich_script = 'get_gene_zscores.from_npz.dev.py'; + this.enrich_script = 'get_gene_zscores.from_hdf5.dev.py'; + + this.svg_width = parseInt(d3.select('svg').attr('width'), 10); + this.svg_height = parseInt(d3.select('svg').attr('height'), 10); + + /* ------------------------------- Gene menu ---------------------------- */ + this.channelsButton = this.menuBar + .append('input') + .attr('id', 'channels_button') + .style('margin-left', '-25px') + .style('visibility', 'hidden') + .attr('type', 'radio'); + + this.greenMenu = this.menuBar + .append('input') + .attr('type', 'text') + .attr('class', 'biginput') + .attr('id', 'autocomplete') + .attr('value', 'Enter gene name') + .style('margin-bottom', '7px') + .on('click', () => { + document.getElementById('gradient_button').checked = false; + document.getElementById('labels_button').checked = false; + document.getElementById('channels_button').checked = true; + this.update_color_menu_tints(); + this.update_slider(); + }); + + /* ------------------------------- Label menu ---------------------------- */ + + this.labelsButton = this.menuBar + .append('input') + .attr('id', 'labels_button') + .attr('type', 'radio') + .style('margin-left', '12px') + .on('click', () => this.labels_click()) + .attr('checked', true); + + this.labelsMenu = this.menuBar + .append('select') + .style('margin-left', '-2px') + .style('font-size', '13px') + .style( + 'background', + 'linear-gradient(to right, rgb(255, 186, 186), rgb(255, 252, 186), rgb(196, 255, 186), rgb(186, 255, 254), rgb(186, 192, 255), rgb(252, 186, 255))', + ) + // .style("background", "linear-gradient(to right, rgb(185, 116, 116), rgb(185, 182, 116), rgb(126, 185, 126), rgb(116, 185, 184), rgb(116, 122, 185), rgb(182, 116, 185))") + .style('background-color', 'rgba(0,0,0,0.5)') + .style('background-blend-mode', 'screen') + .attr('id', 'labels_menu') + .on('change', () => { + this.update_slider(); + }) + .on('click', () => this.labels_click()); + + this.menuBar.selectAll('options').style('font-size', '6px'); + + /* ------------------------------- Gradient menu ---------------------------- */ + + this.gradientButton = this.menuBar + .append('input') + .style('margin-left', '7px') + .attr('id', 'gradient_button') + .attr('type', 'radio') + .on('click', () => this.gradient_click()); + + this.gradientMenu = this.menuBar + .append('select') + .style('margin-left', '-1px') + .style('font-size', '13px') + // .style("background", "linear-gradient(to right, rgb(255, 153, 102), rgb(255, 255, 153))") + .style('background', 'linear-gradient(to right, rgb(185, 83, 32), rgb(185, 185, 83))') + .attr('id', 'gradient_menu') + .on('change', () => { + this.update_slider(); + }) + .on('click', () => this.gradient_click()); + + /* ----------------------------- Populate menus ---------------------------- */ + this.dispatch = d3.dispatch('load', 'statechange'); + this.dispatch.on('load', (data, tag) => { + let select; + if (tag === 'gene_sets') { + select = this.gradientMenu; + } else if (tag === 'all_genes') { + select = this.greenMenu; + } else { + select = this.labelsMenu; + } + select.selectAll('option').remove(); + select + .selectAll('option') + .data(Object.keys(data)) + .enter() + .append('option') + .attr('value', d => { + return d; + }) + .text(d => { + return d; + }); + + this.dispatch.on('statechange', state => { + select.property('value', state.id); + }); + }); + + d3.select('download_ranked_terms').on('click', () => this.downloadRankedTerms()); + d3.select('download_doublet_scores').on('click', () => this.downloadDoubletScores()); + + /* ----------------------------------------------------------------------------------- + Graph coloring + */ + this.gradient_color = d3 + .scaleLinear() + .domain([0, 0.5, 1]) + // @ts-ignore + .range(['black', 'red', 'yellow']); + + this.green_array = new Array(); + this.green_array_raw = new Array(); + + /* ----------------------------------------------------------------------------------- + Color slider + */ + this.yellow_gradient = d3 + .select('svg') + .append('defs') + .append('linearGradient') + .attr('id', 'yellow_gradient') + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '0%') + .attr('spreadMethod', 'pad'); + this.yellow_gradient + .append('stop') + .attr('offset', '0%') + .attr('stop-color', 'black') + .attr('stop-opacity', 1); + this.yellow_gradient + .append('stop') + .attr('offset', '50%') + .attr('stop-color', 'red') + .attr('stop-opacity', 1); + this.yellow_gradient + .append('stop') + .attr('offset', '100%') + .attr('stop-color', 'yellow') + .attr('stop-opacity', 1); + + this.green_gradient = d3 + .select('svg') + .append('defs') + .append('linearGradient') + .attr('id', 'green_gradient') + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '0%') + .attr('spreadMethod', 'pad'); + this.green_gradient + .append('stop') + .attr('offset', '0%') + .attr('stop-color', 'black') + .attr('stop-opacity', 1); + this.green_gradient + .append('stop') + .attr('offset', '100%') + .attr('stop-color', d3.rgb(0, 255, 0).toString()) + .attr('stop-opacity', 1); + + this.slider_scale = d3 + .scaleLinear() + .domain([0, 10]) + .range([0, this.svg_width / 3]) + .clamp(true); + + this.slider = d3 + .select('svg') + .append('g') + .attr('class', 'colorbar_item') + .attr('id', 'slider') + .attr('transform', 'translate(' + this.svg_width / 3 + ',' + 26 + ')'); + + this.current_value = 0; + + this.slider + .append('line') + .attr('class', 'colorbar_item') + .attr('id', 'track') + .attr('x1', this.slider_scale.range()[0]) + .attr('x2', this.slider_scale.range()[1]) + .select(function() { + return this.parentNode.appendChild(this.cloneNode(true)); + }) + .attr('id', 'track-inset') + .select(function() { + return this.parentNode.appendChild(this.cloneNode(true)); + }) + .attr('id', 'track-overlay'); + + this.slider_gradient = this.slider + .append('rect') + .attr('class', 'colorbar_item') + .attr('id', 'gradient_bar') + .attr('fill', 'url(#yellow_gradient)') + .attr('x', -2) + .attr('y', -3.5) + .attr('width', 1) + .attr('height', 7); + + this.handle = this.slider + .insert('circle', '#track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'handle') + .style('fill', '#FFFF99') + .attr('r', 8); + + this.left_bracket = this.slider + .append('rect') + .attr('id', '#track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'left_bracket') + .style('fill', 'yellow') + .attr('width', 6.5) + .attr('height', 21) + .attr('x', 110) + .attr('y', -10) + .style('visibility', 'hidden'); + + this.right_bracket = this.slider + .append('rect') + .attr('id', '#track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'right_bracket') + .style('fill', 'yellow') + .attr('width', 6.5) + .attr('height', 21) + .attr('x', 240) + .attr('y', -10) + .style('visibility', 'hidden'); + + this.left_bracket_label = this.slider + .append('text') + .attr('id', '#track-overlay') + .attr('class', 'bracket_label') + .attr('id', 'left_bracket_label') + .attr('x', 110) + .attr('y', 30) + .style('visibility', 'hidden') + .style('color', 'red') + .text(''); + + this.right_bracket_label = this.slider + .append('text') + .attr('id', '#track-overlay') + .attr('class', 'bracket_label') + .attr('id', 'right_bracket_label') + .attr('x', 240) + .attr('y', 30) + .style('visibility', 'hidden') + .text(''); + + this.ceiling_bracket = this.slider + .append('rect') + .attr('id', '#track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'ceiling_bracket') + .style('fill', 'yellow') + .attr('width', 136.5) + .attr('height', 5) + .attr('x', 110) + .attr('y', -12) + .style('visibility', 'hidden'); + + this.floor_bracket = this.slider + .append('rect') + .attr('id', '#track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'floor_bracket') + .style('fill', 'yellow') + .attr('width', 136.5) + .attr('height', 5) + .attr('x', 110) + .attr('y', 6) + .style('visibility', 'hidden'); + + this.slider_ticks = this.slider + .insert('g', '#track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'ticks') + .attr('transform', 'translate(0,' + 18 + ')') + .selectAll('text') + .data(this.slider_scale.ticks(10)) + .enter() + .append('text') + .attr('x', this.slider_scale) + .attr('text-anchor', 'middle') + .text(d => { + return d; + }); + + d3.select('#legend') + .style('left', (this.svg_width - 224).toString() + 'px') + .style('height', (this.svg_height - 158).toString() + 'px'); + + this.legendMask = d3 + .select('svg') + .append('rect') + .attr('class', 'colorbar_item') + .attr('id', 'legend_mask') + .attr('x', this.svg_width) + .attr('y', 158) + .attr('fill-opacity', 0.35) + .attr('width', 405) + .attr('height', d3.select('svg').attr('height')); + + d3.select('#slider_select_button') + .select('button') + .on('click', () => this.toggle_slider_select()); + + /* ----------------------------------------------------------------------------------- + Create button for showing enriched gene set for a selection + */ + + this.rankedTermsButtonRect = d3 + .select('svg') + .append('rect') + .attr('class', 'colorbar_item') + .attr('x', -70) + .attr('y', 0) + .attr('fill-opacity', 0.35) + .attr('width', 200) + .attr('height', 24) + .on('click', () => { + this.showRankedTerms(); + }); + + this.rankedTermsButtonLabel = d3 + .select('svg') + .append('text') + .attr('class', 'colorbar_item') + .attr('x', 6) + .attr('y', 16) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'white') + .text('Show enriched terms') + .attr('pointer-events', 'none'); + + this.exoutTermsButtonLabel = d3 + .select('svg') + .append('text') + .attr('class', 'colorbar_item') + .attr('x', 180) + .attr('y', 17) + .attr('font-family', 'sans-serif') + .attr('font-size', '14px') + .attr('fill', 'white') + .attr('width', 40) + .text('X') + .style('opacity', 0) + .attr('pointer-events', 'none'); + + this.exoutTermsButton = d3 + .select('svg') + .append('rect') + .attr('class', 'colorbar_item') + .attr('x', 170) + .attr('y', 0) + .attr('width', 30) + .attr('height', 30) + .attr('fill-opacity', 0) + .on('click', () => { + this.hideRankedTerms(); + }); + + this.slider.call( + d3 + .drag() + .on('start', () => { + const cx = d3.event.sourceEvent.x - this.svg_width / 3; + // console.log(svg_width, d3.select("#track").attr("x")); + if ( + Math.abs(cx - parseFloat(this.left_bracket.attr('x')) - 12) < 5 && + d3.select('#left_bracket').style('visibility') === 'visible' + ) { + this.drag_mode = 'left_bracket'; + // console.log(cx, parseFloat(left_bracket.attr("x"))); + } else if ( + Math.abs(cx - parseFloat(this.right_bracket.attr('x')) - 12) < 5 && + d3.select('#right_bracket').style('visibility') === 'visible' + ) { + this.drag_mode = 'right_bracket'; + // console.log(cx, parseFloat(right_bracket.attr("x"))); + } else { + this.drag_mode = 'handle'; + } + }) + .on('drag', () => { + const cx = d3.event.x - this.svg_width / 3; + if (this.drag_mode === 'left_bracket') { + this.set_left_bracket(cx); + selectionScript.update_selected_count(); + } + if (this.drag_mode === 'right_bracket') { + this.set_right_bracket(cx); + selectionScript.update_selected_count(); + } else if (this.drag_mode === 'handle') { + this.set_slider_position(cx); + } + }) + .on('end', () => { + this.slider.interrupt(); + }), + ); + + this.all_gene_color_array = {}; + this.all_gene_cellix_array = {}; + + $('#autocomplete').blur(() => { + if (this.gene_entered) { + document.getElementById('autocomplete').value = this.last_gene; + } else { + document.getElementById('autocomplete').value = 'Enter gene name'; + } + }); + + $('#autocomplete').focus(() => { + if (!this.gene_entered) { + document.getElementById('autocomplete').value = ''; + } + }); + + this.geneAutocomplete(color_menu_genes); + + /* ----------------------------------------------------------------------------------- + Create button for showing enriched gene for a selection + */ + d3.select('#termsheet').attr('height', this.svg_height - 5); + + this.rankedGenesButtonRect = d3 + .select('svg') + .append('rect') + .attr('class', 'colorbar_item') + .attr('id', 'rankedGenesButton') + .attr('x', -70) + .attr('y', 24) + .attr('fill-opacity', 0.35) + .attr('width', 200) + .attr('height', 24) + .on('click', () => { + this.showRankedGenes(); + }); + + this.rankedGenesButtonLabel = d3 + .select('svg') + .append('text') + .attr('class', 'colorbar_item') + .attr('x', 6) + .attr('y', 40) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'white') + .text('Show enriched genes') + .attr('pointer-events', 'none'); + + this.rankedMask = d3 + .select('svg') + .append('rect') + .attr('class', 'colorbar_item') + .attr('x', -200) + .attr('y', 48) + .attr('fill-opacity', 0.35) + .style('color', 'gray') + .attr('width', 200) + .attr('height', d3.select('svg').attr('height')); + + this.exoutGenesButtonLabel = d3 + .select('svg') + .append('text') + .attr('class', 'colorbar_item') + .attr('x', 180) + .attr('y', 41) + .attr('font-family', 'sans-serif') + .attr('font-size', '14px') + .attr('fill', 'white') + .attr('width', 40) + .text('X') + .attr('pointer-events', 'none') + .style('opacity', 0); + + this.exoutGenesButton = d3 + .select('svg') + .append('rect') + .attr('class', 'colorbar_item') + .attr('x', 170) + .attr('y', 24) + .attr('width', 40) + .attr('height', 30) + .attr('fill-opacity', 0) + .on('click', () => { + this.hideRankedGenes(); + }); + + return this; + } + // <-- ColorBar Constructor End --> + + async loadData() { + // open json file containing gene sets and populate drop down menu + this.categorical_coloring_data = await d3.json( + project_directory + '/categorical_coloring_data.json' + '?_=' + this.noCache, + ); + Object.keys(this.categorical_coloring_data).forEach(k => { + const label_counts = {}; + Object.keys(this.categorical_coloring_data[k].label_colors).forEach(n => { + label_counts[n] = 0; + }); + this.categorical_coloring_data[k].label_list.forEach(n => { + label_counts[n] += 1; + }); + this.categorical_coloring_data[k].label_counts = label_counts; + }); + + this.dispatch.call('load', this, this.categorical_coloring_data, 'cell_labels'); + this.update_slider(); + + this.color_stats = await d3.json(project_directory + '/color_stats.json' + '?_=' + this.noCache); + this.addStreamExp(this.color_menu_genes); + + this.last_gene = ''; + this.gene_entered = false; + + // open json file containing gene sets and populate drop down menu + const text = await d3.text(project_directory + '/color_data_gene_sets.csv' + '?_=' + this.noCache); + this.gene_set_color_array = this.read_csv(text); + + // gradientMenu.selectAll("option").remove(); + this.dispatch.call('load', this, this.gene_set_color_array, 'gene_sets'); + // update_slider(); + } + + normalize(x) { + const min = 0; + const max = this.color_max; + const out = []; + for (let i = 0; i < x.length; i++) { + if (x[i] > max) { + out.push(1); + } else { + out.push((x[i] - min) / (max - min)); + } + } + return out; + } + + normalize_one_val(x) { + const min = 0; + const max = this.color_max; + return x > max ? 1 : (x - min) / (max - min); + } + + update_tints() { + for (let i = 0; i < forceLayout.base_colors.length; i++) { + const rgb = forceLayout.base_colors[i]; + forceLayout.all_nodes[i].tint = rgbToHex(rgb.r, rgb.g, rgb.b); + } + } + + setNodeColors() { + if (document.getElementById('gradient_button').checked) { + const current_selection = document.getElementById('gradient_menu').value; + const color_array = this.normalize(this.gene_set_color_array[current_selection]); + + for (let i = 0; i < forceLayout.base_colors.length; i++) { + forceLayout.base_colors[i] = d3.rgb(this.gradient_color(color_array[i])); + } + this.update_tints(); + } + if (document.getElementById('labels_button').checked) { + const name = document.getElementById('labels_menu').value; + const cat_color_map = this.getSampleCategoricalColoringData(name).label_colors; + const cat_label_list = this.getSampleCategoricalColoringData(name).label_list; + + for (let i = 0; i < forceLayout.base_colors.length; i++) { + forceLayout.base_colors[i] = d3.rgb(cat_color_map[cat_label_list[i]]); + } + this.update_tints(); + } + if (document.getElementById('channels_button').checked) { + const t0 = new Date(); + const green_selection = document.getElementById('autocomplete').value; + console.log(green_selection); + $.ajax({ + data: { base_dir: graph_directory, sub_dir: project_directory, gene: green_selection }, + success: data => { + const t1 = new Date(); + console.log('Read gene data: ', t1.getTime() - t0.getTime()); + this.green_array = data.split('\n').slice(0, -1); + this.green_array_raw = data.split('\n').slice(0, -1); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + const rawval = this.green_array[i]; + const gg = this.normalize_one_val(rawval); + forceLayout.base_colors[i] = { r: 0, g: Math.floor(gg * 255), b: 0 }; + } + + forceLayout.app.stage.children[1].children.sort((a, b) => { + return this.green_array[a.index] - this.green_array[b.index]; + }); + + this.update_tints(); + if (d3.select('#left_bracket').style('visibility') === 'visible') { + this.slider_select_update(); + selectionScript.update_selected_count(); + } + }, + type: 'POST', + url: 'cgi-bin/grab_one_gene.py', + }); + } + } + + updateColorMax() { + if (document.getElementById('gradient_button').checked) { + const current_selection = document.getElementById('gradient_menu').value; + + const color_array = this.normalize(this.gene_set_color_array[current_selection]); + for (let i = 0; i < forceLayout.base_colors.length; i++) { + forceLayout.base_colors[i] = d3.rgb(this.gradient_color(color_array[i])); + } + this.update_tints(); + } + if (document.getElementById('channels_button').checked) { + for (let i = 0; i < forceLayout.base_colors.length; i++) { + const gg = this.normalize_one_val(this.green_array[i]); + forceLayout.base_colors[i] = { r: 0, g: Math.floor(gg * 255), b: 0 }; + } + this.update_tints(); + } + if (document.getElementById('labels_button').checked) { + for (let i = 0; i < forceLayout.base_colors.length; i++) { + const rr = Math.floor(this.normalize_one_val(forceLayout.base_colors[i].r) * 255); + const gg = Math.floor(this.normalize_one_val(forceLayout.base_colors[i].g) * 255); + const bb = Math.floor(this.normalize_one_val(forceLayout.base_colors[i].b) * 255); + forceLayout.all_nodes[i].tint = rgbToHex(rr, gg, bb); + } + // update_tints(); + } + } + + labels_click() { + document.getElementById('gradient_button').checked = false; + document.getElementById('labels_button').checked = true; + document.getElementById('channels_button').checked = false; + this.update_color_menu_tints(); + this.update_slider(); + } + + gradient_click() { + document.getElementById('gradient_button').checked = true; + document.getElementById('channels_button').checked = false; + document.getElementById('labels_button').checked = false; + this.update_color_menu_tints(); + this.update_slider(); + } + + update_color_menu_tints() { + d3.select('#autocomplete').style('background-color', 'rgb(130,200,130)'); + d3.select('#labels_menu').style( + 'background', + 'linear-gradient(to right, rgb(185, 116, 116), rgb(185, 182, 116), rgb(126, 185, 126), rgb(116, 185, 184), rgb(116, 122, 185), rgb(182, 116, 185))', + ); + d3.select('#gradient_menu').style('background', 'linear-gradient(to right, rgb(185, 83, 32), rgb(185, 185, 83))'); + + if (document.getElementById('gradient_button').checked) { + d3.select('#gradient_menu').style( + 'background', + 'linear-gradient(to right, rgb(255, 153, 102), rgb(255, 255, 153))', + ); + } + if (document.getElementById('labels_button').checked) { + d3.select('#labels_menu').style( + 'background', + 'linear-gradient(to right, rgb(255, 186, 186), rgb(255, 252, 186), rgb(196, 255, 186), rgb(186, 255, 254), rgb(186, 192, 255), rgb(252, 186, 255))', + ); + } + if (document.getElementById('channels_button').checked) { + d3.select('#autocomplete').style('background-color', '#b3ffb3'); + } + } + + toggle_slider_select() { + if (d3.select('#slider_select_button').style('stroke') === 'none') { + this.show_slider_select(); + } else { + this.hide_slider_select(); + } + selectionScript.update_selected_count(); + } + + show_slider_select() { + d3.select('#slider_select_button') + .style('fill-opacity', 0.7) + .style('stroke', 'yellow'); + d3.select('#left_bracket').style('visibility', 'visible'); + d3.select('#right_bracket').style('visibility', 'visible'); + d3.select('#floor_bracket').style('visibility', 'visible'); + d3.select('#ceiling_bracket').style('visibility', 'visible'); + d3.select('#right_bracket_label').style('visibility', 'visible'); + d3.select('#left_bracket_label').style('visibility', 'visible'); + this.slider_select_update(); + } + + hide_slider_select() { + d3.select('#slider_select_button') + .style('fill-opacity', 0.25) + .style('stroke', 'none'); + d3.select('#left_bracket').style('visibility', 'hidden'); + d3.select('#right_bracket').style('visibility', 'hidden'); + d3.select('#floor_bracket').style('visibility', 'hidden'); + d3.select('#ceiling_bracket').style('visibility', 'hidden'); + d3.select('#right_bracket_label').style('visibility', 'hidden'); + d3.select('#left_bracket_label').style('visibility', 'hidden'); + } + + set_left_bracket(h) { + const cx = this.slider_scale(this.slider_scale.invert(h)); + const w = parseInt(d3.select('#right_bracket').attr('x'), 10) - cx + 6.5; + if (w > 12) { + d3.select('#left_bracket').attr('x', cx); + this.floor_bracket.attr('x', cx).style('width', w + 'px'); + this.ceiling_bracket.attr('x', cx).style('width', w + 'px'); + this.left_bracket_label.attr('x', cx); + this.slider_select_update(); + } + } + + set_right_bracket(h) { + const cx = this.slider_scale(this.slider_scale.invert(h)); + const w = cx - parseInt(d3.select('#left_bracket').attr('x'), 10) + 6.5; + if (w > 12) { + d3.select('#right_bracket').attr('x', cx); + this.floor_bracket.style('width', w + 'px'); + this.ceiling_bracket.style('width', w + 'px'); + this.right_bracket_label.attr('x', cx); + this.slider_select_update(); + } + } + + slider_select_update() { + const lower_bound = this.slider_scale.invert(this.left_bracket.attr('x')); + const upper_bound = this.slider_scale.invert(this.right_bracket.attr('x')); + this.left_bracket_label.text(lower_bound.toFixed(2)); + this.right_bracket_label.text(upper_bound.toFixed(2)); + + let color_array = null; + if (document.getElementById('gradient_button').checked) { + const current_selection = document.getElementById('gradient_menu').value; + color_array = this.gene_set_color_array[current_selection]; + } + if (document.getElementById('channels_button').checked) { + this.green_selection = d3.select('#autocomplete').node().value; + color_array = this.green_array; + } + if (document.getElementById('labels_button').checked) { + color_array = forceLayout.base_colors.map(this.average_color); + } + if (color_array != null) { + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + const x = color_array[i]; + if (x >= lower_bound && (x <= upper_bound || upper_bound > this.slider_scale.domain()[1] * 0.98)) { + forceLayout.all_outlines[i].selected = true; + forceLayout.all_outlines[i].compared = false; + forceLayout.all_outlines[i].alpha = forceLayout.all_nodes[i].alpha; + forceLayout.all_outlines[i].tint = '0xffff00'; + } else { + forceLayout.all_outlines[i].selected = false; + forceLayout.all_outlines[i].alpha = 0; + } + } + } + } + + update_slider() { + d3.select('#label_column') + .selectAll('div') + .remove(); + d3.select('#count_column') + .selectAll('div') + .remove(); + if (document.getElementById('labels_button').checked) { + d3.selectAll('#gradient_bar').attr('fill', '#7e7e7e'); + d3.selectAll('#handle').style('fill', '#7e7e7e'); + const name = document.getElementById('labels_menu').value; + + let cat_color_map = this.getSampleCategoricalColoringData(name).label_colors; + let cat_label_list = this.getSampleCategoricalColoringData(name).label_list; + d3.select('#legend_mask') + .transition() + .attr('x', this.svg_width - 177) + .on('end', () => { + this.make_legend(cat_color_map, cat_label_list); + }); + + let max = parseInt(d3.max(forceLayout.base_colors.map(this.max_color)), 10); + if (max === 0) { + max = 255; + } + this.color_max = max; + this.slider_scale.domain([0, max * 1.05]); + + this.set_slider_position_only(max); + } else { + d3.select('#legend_mask') + .transition() + .attr('x', this.svg_width); + if (this.color_stats == null) { + return; + } + let geneName = ''; + if (document.getElementById('gradient_button').checked) { + geneName = document.getElementById('gradient_menu').value; + d3.selectAll('#gradient_bar').attr('fill', 'url(#yellow_gradient)'); + d3.selectAll('#handle').style('fill', '#FFFF99'); + } else { + // const name = document.getElementById('green_menu').value; + geneName = document.getElementById('autocomplete').value; + d3.selectAll('#gradient_bar').attr('fill', 'url(#green_gradient)'); + d3.selectAll('#handle').style('fill', d3.rgb(0, 255, 0).toString()); + } + if (this.color_stats[geneName]) { + const max = this.color_stats[geneName][3]; + this.slider_scale.domain([0, max * 1.05]); + this.set_slider_position_only(this.slider_scale(this.color_stats[geneName][4])); + } + } + + if (this.slider_ticks) { + this.slider_ticks.remove(); + d3.select('.ticks').remove(); + } + + let ticknum = 0; + + if (this.color_max < 1) { + ticknum = this.color_max * 10; + } else if (this.color_max < 2) { + ticknum = this.color_max * 5; + } else if (this.color_max < 10) { + ticknum = this.color_max; + } else if (this.color_max < 50) { + ticknum = this.color_max / 5; + } else if (this.color_max < 100) { + ticknum = this.color_max / 10; + } else if (this.color_max < 200) { + ticknum = this.color_max / 20; + } else if (this.color_max < 1000) { + ticknum = this.color_max / 100; + } else if (this.color_max < 20000) { + ticknum = this.color_max / 1000; + } else if (this.color_max < 200000) { + ticknum = this.color_max / 10000; + } else if (this.color_max < 2000000) { + ticknum = this.color_max / 100000; + } else if (this.color_max < 20000000) { + ticknum = this.color_max / 1000000; + } + + this.slider_ticks = this.slider + .insert('g', '.track-overlay') + .attr('class', 'colorbar_item') + .attr('id', 'ticks') + .attr('transform', 'translate(0,' + 18 + ')') + .selectAll('text') + .data(this.slider_scale.ticks(ticknum)) + .enter() + .append('text') + .attr('x', this.slider_scale) + .attr('text-anchor', 'middle') + .text(d => { + return d; + }); + + if (document.getElementById('gradient_button').checked) { + d3.select('.ticks') + .append('text') + .attr('x', this.svg_width / 3 + 10) + .text('Z-score'); + } else { + d3.select('.ticks') + .append('text') + .attr('x', this.svg_width / 3 + 10) + .text('UMIs'); + } + this.setNodeColors(); + if (this.left_bracket.style('visibility') === 'visible') { + this.slider_select_update(); + } + } + + set_slider_position(h) { + this.handle.attr('cx', this.slider_scale(this.slider_scale.invert(h))); + this.slider_gradient.attr('width', Math.max(this.slider_scale(this.slider_scale.invert(h)) - 6, 0)); + this.color_max = this.slider_scale.invert(h); + this.updateColorMax(); + } + + set_slider_position_only(h) { + this.handle.attr('cx', this.slider_scale(this.slider_scale.invert(h))); + this.slider_gradient.attr('width', Math.max(this.slider_scale(this.slider_scale.invert(h)) - 6, 0)); + this.color_max = this.slider_scale.invert(h); + } + + /* ----------------------------------------------------------------------------------- + Load expression data + */ + + read_csv(text) { + let dict = {}; + text.split('\n').forEach((entry, index, array) => { + if (entry.length > 0) { + let items = entry.split(','); + let gene = items[0]; + let exp_array = []; + items.forEach((e, i, a) => { + if (i > 0) { + exp_array.push(parseFloat(e)); + } + }); + dict[gene] = exp_array; + } + }); + return dict; + } + + addStreamExp(gene_list) { + const tmpdict = {}; + gene_list.split('\n').forEach(g => { + if (g.length > 0) { + tmpdict[g] = 0; + } + }); + this.dispatch.call('load', this, tmpdict, 'all_genes'); + } + + getSampleCategoricalColoringData(key) { + return this.categorical_coloring_data[key]; + } + + geneAutocomplete(gene_list) { + const gene_lookup = []; + gene_list.split('\n').forEach(g => { + if (g.length > 0) { + gene_lookup.push({ value: g, data: g }); + } + }); + console.log('# genes = ', gene_lookup.length); + + $('#autocomplete').autocomplete({ + lookup: gene_lookup, + onSelect: suggestion => { + const submitGene = suggestion.data; + document.getElementById('autocomplete').value = submitGene; + document.getElementById('gradient_button').checked = false; + document.getElementById('labels_button').checked = false; + document.getElementById('channels_button').checked = true; + this.update_slider(); + this.last_gene = submitGene; + this.gene_entered = true; + }, + }); + $('#autocomplete').keydown(event => { + if (event.keyCode === 13) { + const submitGene = document.getElementById('autocomplete').value; + document.getElementById('gradient_button').checked = false; + document.getElementById('labels_button').checked = false; + document.getElementById('channels_button').checked = true; + this.update_slider(); + this.last_gene = submitGene; + this.gene_entered = true; + } + }); + } + + showRankedGenes() { + if (this.color_stats != null) { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + const snd = new Audio('src/sound_effects/openclose_sound.wav'); + snd.play(); + } + // setNodeColors(); + this.hideRankedTerms(); + d3.select('#termsheet').style('left', '10px'); + d3.select('#termcolumn') + .selectAll('div') + .remove(); + d3.select('#scorecolumn') + .selectAll('div') + .remove(); + this.rankedMask + .transition() + .attr('x', 0) + .each(() => { + this.renderRankedText(this.all_gene_color_array, 1); + }); + this.rankedGenesButtonRect.transition().attr('x', 0); + this.rankedTermsButtonRect.transition().attr('x', 0); + this.exoutGenesButtonLabel + .transition() + .delay(200) + .style('opacity', 1); + } + } + + hideRankedGenes() { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + const snd = new Audio('src/sound_effects/openclose_sound.wav'); + snd.play(); + } + d3.select('#termsheet').style('left', '-200px'); + d3.select('#termcolumn') + .selectAll('div') + .remove(); + d3.select('#scorecolumn') + .selectAll('div') + .remove(); + this.rankedMask.transition().attr('x', -200); + this.rankedTermsButtonRect.transition().attr('x', -70); + this.rankedGenesButtonRect.transition().attr('x', -70); + this.exoutGenesButtonLabel.style('opacity', 0); + } + + showRankedTerms() { + if (this.color_stats != null) { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + const snd = new Audio('src/sound_effects/openclose_sound.wav'); + snd.play(); + } + // setNodeColors(); + this.hideRankedGenes(); + d3.select('#termsheet').style('left', '10px'); + d3.select('#termcolumn') + .selectAll('div') + .remove(); + d3.select('#scorecolumn') + .selectAll('div') + .remove(); + this.rankedMask + .transition() + .attr('x', 0) + .each(() => { + this.renderRankedText(this.gene_set_color_array, 0); + }); + this.rankedGenesButtonRect.transition().attr('x', 0); + this.rankedTermsButtonRect.transition().attr('x', 0); + this.exoutTermsButtonLabel + .transition() + .delay(200) + .style('opacity', 1); + } + } + + hideRankedTerms() { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + const snd = new Audio('src/sound_effects/openclose_sound.wav'); + snd.play(); + } + d3.select('#termsheet').style('left', '-200px'); + d3.select('#termcolumn') + .selectAll('div') + .remove(); + d3.select('#scorecolumn') + .selectAll('div') + .remove(); + this.rankedMask.transition().attr('x', -200); + this.rankedTermsButtonRect.transition().attr('x', -70); + this.rankedGenesButtonRect.transition().attr('x', -70); + this.exoutTermsButtonLabel.style('opacity', 0); + } + + renderRankedText(tracks, version) { + let any_selected = false; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected || forceLayout.all_outlines[i].compared) { + any_selected = true; + } + } + if (!any_selected) { + d3.select('#termcolumn') + .append('div') + .append('p') + .text('No cells selected'); + } else { + this.getRankedText(tracks, version); + } + } + + actuallyRenderRankedText(rankedText) { + let scorecol = rankedText[1]; + let termcol = []; + rankedText[0].forEach(d => { + const term = d; + termcol.push(term); + }); + + d3.select('#termcolumn') + .selectAll('div') + .data(termcol) + .enter() + .append('div') + .append('p') + .text(d => { + if (d.length < 20) { + return d; + } else { + return d.slice(0, 17) + '...'; + } + }); + + d3.select('#scorecolumn') + .selectAll('div') + .data(scorecol) + .enter() + .append('div') + .append('p') + .text(d => { + return d; + }); + + d3.select('#termcolumn') + .selectAll('div') + .style('background-color', 'rgba(0, 0, 0, 0)') + .on('mouseover', d => { + d3.select(d).style('background-color', 'rgba(0, 0, 0, 0.3)'); + }) + .on('mouseout', d => { + d3.select(d).style('background-color', 'rgba(0, 0, 0, 0)'); + }) + .on('click', d => { + if (this.exoutGenesButtonLabel.style('opacity') === '1') { + document.getElementById('channels_button').checked = true; + document.getElementById('gradient_button').checked = false; + document.getElementById('labels_button').checked = false; + document.getElementById('autocomplete').value = d.toString(); + // d3.select("#green_menu")[0][0].value = d; + // $("#autocomplete").attr("value", d);// = d; + } + if (this.exoutTermsButtonLabel.style('opacity') === '1') { + document.getElementById('channels_button').checked = false; + document.getElementById('gradient_button').checked = true; + document.getElementById('labels_button').checked = false; + d3.select('#gradient_menu').node().value = d; + } + this.update_slider(); + }); + } + + // preload_enrichments(); + preload_enrichments() { + let sel2text = ''; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + sel2text = sel2text + ',' + i.toString(); + } + sel2text = sel2text.slice(1, sel2text.length); + const t0 = new Date(); + console.log('Preloading enrichments'); + $.ajax({ + data: { + base_dir: graph_directory, + compared_cells: '', + selected_cells: sel2text, + sub_dir: project_directory, + }, + success: data => { + const t1 = new Date(); + console.log('Preloaded enrichments: ', t1.getTime() - t0.getTime()); + this.enrich_script = 'get_gene_zscores.from_hdf5.dev.py'; + }, + type: 'POST', + url: 'cgi-bin/get_gene_zscores.from_hdf5.dev.py', + }); + } + + getRankedText(tracks, version) { + const selected_nodes = []; + const compared_nodes = []; + for (const i in forceLayout.all_outlines) { + if (forceLayout.all_outlines[i].selected) { + selected_nodes.push(i); + } + if (forceLayout.all_outlines[i].compared) { + compared_nodes.push(i); + } + } + let scoremap = d3.map(); + let scoretotal = 0; + let selected_score = 0; + let compared_score = 0; + let dat = null; + + if (version === 0) { + for (const term in tracks) { + if (selected_nodes.length > 0 || compared_nodes.length > 0) { + dat = tracks[term]; + } + if (selected_nodes.length === 0) { + selected_score = 0; + } else { + selected_score = this.getTermScore(dat, selected_nodes) / selected_nodes.length; + selected_score = (selected_score - this.color_stats[term][0]) / (this.color_stats[term][1] + 0.02); + } + if (compared_nodes.length === 0) { + compared_score = 0; + } else { + compared_score = this.getTermScore(dat, compared_nodes) / compared_nodes.length; + compared_score = (compared_score - this.color_stats[term][0]) / (this.color_stats[term][1] + 0.02); + } + scoremap[term] = selected_score - compared_score; + } + let tuples = []; + for (const key in scoremap) { + if (typeof key === 'string') { + if (key.length > 1) { + tuples.push([key, scoremap[key]]); + } + } + } + tuples.sort((a, b) => { + return b[1] - a[1]; + }); + let termcol = ['Term']; + let scorecol = ['Z-score']; + + tuples.forEach(d => { + let numline = d[1].toString().slice(0, 5); + termcol.push(d[0]); + scorecol.push(numline); + }); + this.actuallyRenderRankedText([termcol.slice(0, 1000), scorecol.slice(0, 1000)]); + } else { + let sel2text = ''; + let comp2text = ''; + let n_highlight = 0; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + sel2text = sel2text + ',' + i.toString(); + n_highlight = n_highlight + 1; + } + if (forceLayout.all_outlines[i].compared) { + comp2text = comp2text + ',' + i.toString(); + n_highlight = n_highlight + 1; + } + } + if (sel2text.length > 0) { + sel2text = sel2text.slice(1, sel2text.length); + } + if (comp2text.length > 0) { + comp2text = comp2text.slice(1, comp2text.length); + } + + // if (all_nodes.length > 0) {const script="get_gene_zscores.from_npz.dev.py"; console.log("npz enrichment");} + // else {const script="get_gene_zscores.from_hdf5.dev.py"; console.log("hdf5 enrichment");} + // if (n_highlight > 500) {const script="get_gene_zscores.from_npz.py"; console.log("npz enrichment");} + // else {const script="get_gene_zscores.py"; console.log("hdf5 enrichment");} + this.enrich_script = 'get_gene_zscores.from_hdf5.dev.py'; + const t0 = new Date(); + console.log(this.enrich_script); + $.ajax({ + data: { + base_dir: graph_directory, + compared_cells: comp2text, + selected_cells: sel2text, + sub_dir: project_directory, + }, + success: data => { + const t1 = new Date(); + console.log(t1.getTime() - t0.getTime()); + data = data.split('\t'); + let termcol = data[0].split('\n'); + let scorecol = data[1].split('\n').slice(0, -1); + this.actuallyRenderRankedText([termcol, scorecol]); + }, + type: 'POST', + url: 'cgi-bin/' + this.enrich_script, + }); + } + } + + getTermScore(a, nodes) { + let score = 0; + nodes.forEach(i => { + score = score + (a[i] + 0.01); + }); + return score; + } + + downloadFile(text, name) { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + const snd = new Audio('src/sound_effects/download_sound.wav'); + snd.play(); + } + const hiddenElement = document.createElement('a'); + hiddenElement.href = 'data:attachment/text,' + encodeURI(text); + hiddenElement.target = '_blank'; + hiddenElement.download = name; + hiddenElement.click(); + } + + downloadFileDirect(path, filename) { + console.log(path); + var hiddenElement = document.createElement('a'); + // hiddenElement.href = '' + hiddenElement.href = path; + hiddenElement.target = '_blank'; + hiddenElement.download = filename; + hiddenElement.click(); + } + + downloadRankedTerms = () => { + let num_selected = 0; + let termcol = []; + let scorecol = []; + let tracks = this.all_gene_color_array; + let sparse_version = 1; + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (forceLayout.all_outlines[i].selected) { + num_selected += 1; + } + } + let text = ''; + if (num_selected === 0 && this.rankedMask.attr('x') === '-200') { + text = 'No cells selected!'; + } else { + if (this.rankedMask.attr('x') === '-200') { + if (document.getElementById('gradient_button').checked) { + tracks = this.gene_set_color_array; + sparse_version = 0; + } else { + tracks = this.all_gene_color_array; + sparse_version = 1; + } + let rankedTerms = this.getRankedText(tracks, sparse_version).slice(0, 1000); + termcol = rankedTerms[0]; + scorecol = rankedTerms[1]; + } else { + termcol = []; + scorecol = []; + d3.select('#termcolumn') + .selectAll('div') + .each(d => { + termcol.push(d); + }); + d3.select('#scorecolumn') + .selectAll('div') + .each(d => { + scorecol.push(d); + }); + } + text = ''; + termcol.forEach((d, i) => { + text = text + '\n' + d + '\t' + scorecol[i]; + }); + text = text.slice(1, text.length); + } + this.downloadFile(text, 'enriched_terms.txt'); + }; + + downloadDoubletScores = () => { + this.downloadFileDirect(`${project_directory}/doublet_results.tsv`, 'doublet_results.tsv'); + }; + + make_legend(cat_color_map, cat_label_list) { + d3.select('#count_column') + .selectAll('div') + .data(Object.keys(cat_color_map)) + .enter() + .append('div') + .style('display', 'inline-block') + .attr('class', 'text_count_div') + .style('height', '25px') + .style('margin-top', '0px') + .style('width', '48px') + .style('overflow', 'hidden') + .append('p') + .text(''); + + d3.select('#count_column') + .on('mouseenter', () => { + d3.selectAll('.text_count_div').each(function() { + var pct = d3 + .select(this) + .select('p') + .text(d3.select(this).attr('pct') + '%'); + }); + }) + .on('mouseleave', () => { + d3.selectAll('.text_count_div').each(function() { + d3.select(this) + .select('p') + .text(d3.select(this).attr('count')); + }); + }); + + d3.select('#label_column') + .selectAll('div') + .data(Object.keys(cat_color_map)) + .enter() + .append('div') + .style('display', 'inline-block') + .attr('class', 'legend_row') + .style('height', '25px') + .style('margin-top', '0px') + .style('width', '152px'); + // .style("overflow","hidden"); + + d3.select('#label_column') + .selectAll('div') + .data(Object.keys(cat_color_map)) + .enter() + .append('div') + .style('display', 'inline-block') + .attr('class', 'legend_row') + .style('height', '25px') + .style('margin-top', '0px') + .style('width', '152px'); + //.style("overflow","hidden"); + + d3.select('#label_column') + .selectAll('div') + .each((d, i, nodes) => { + d3.select(nodes[i]) + .append('div') + .style('background-color', cat_color_map[d]) + .on('click', () => { + show_colorpicker_popup(d); + }); + d3.select(nodes[i]) + .append('div') + .attr('class', 'text_label_div') + .append('p') + .text(d) + .style('float', 'left') + .style('white-space', 'nowrap') + .style('margin-top', '-6px') + .style('margin-left', '3px') + .on('click', () => { + this.categorical_click(d, cat_label_list); + }); + }); + + d3.selectAll('.legend_row') + .style('width', '152px') + .style('background-color', 'rgba(0, 0, 0, 0)') + .on('mouseover', function(d) { + d3.select(this).style('background-color', 'rgba(0, 0, 0, 0.3)'); + }) + .on('mouseout', function(d) { + d3.select(this).style('background-color', 'rgba(0, 0, 0, 0)'); + }); + + d3.selectAll('.legend_row') + .selectAll('p') + .style('width', '150px'); + + this.count_clusters(); + } + + categorical_click(selectedLabel, cat_label_list) { + this.all_selected = true; + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (cat_label_list[i] === selectedLabel) { + if (!(forceLayout.all_outlines[i].selected || forceLayout.all_outlines[i].compared)) { + this.all_selected = false; + } + } + } + + const my_nodes = []; + const indices = []; + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (cat_label_list[i] === selectedLabel) { + my_nodes.push(i); + if (this.all_selected) { + forceLayout.all_outlines[i].selected = false; + forceLayout.all_outlines[i].compared = false; + forceLayout.all_outlines[i].alpha = 0; + } else { + if (selectionScript && selectionScript.selection_mode === 'negative_select') { + forceLayout.all_outlines[i].compared = true; + forceLayout.all_outlines[i].tint = '0x0000ff'; + forceLayout.all_outlines[i].alpha = forceLayout.all_nodes[i].alpha; + } else { + forceLayout.all_outlines[i].selected = true; + forceLayout.all_outlines[i].tint = '0xffff00'; + forceLayout.all_outlines[i].alpha = forceLayout.all_nodes[i].alpha; + } + } + } + if (forceLayout.all_outlines[i].selected) { + indices.push(i); + } + } + + if (forceLayout.all_nodes.length < 25000) { + this.shrinkNodes(6, 10, my_nodes, forceLayout.all_nodes); + } + + if (selectionScript) { + selectionScript.update_selected_count(); + } + + postSelectedCellUpdate(indices); + + this.count_clusters(); + } + + count_clusters() { + const name = document.getElementById('labels_menu').value; + if (name.length > 0) { + const cat_color_map = this.getSampleCategoricalColoringData(name).label_colors; + const cat_label_list = this.getSampleCategoricalColoringData(name).label_list; + const cat_counts = this.getSampleCategoricalColoringData(name).label_counts; + + let counts = {}; + Object.keys(cat_color_map).forEach(d => { + counts[d] = 0; + }); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (forceLayout.all_outlines[i].selected || forceLayout.all_outlines[i].compared) { + counts[cat_label_list[i]] += 1; + } + } + + d3.select('#count_column') + .selectAll('div') + .each((d, i, nodes) => { + d3.select(nodes[i]) + .style('visibility', 'hidden') + .select('p') + .text(''); + if (counts[d] > 0) { + d3.select(nodes[i]) + .attr('count', counts[d]) + .attr('pct', Math.floor((counts[d] / cat_counts[d]) * 1000) / 10) + .style('visibility', 'visible') + .select('p') + .text(counts[d]); + } + }); + } + } + + shrinkNodes(scale, numsteps, my_nodes, all_nodes) { + const current_radii = {}; + const nodes = []; + for (let ii in my_nodes) { + // console.log(['A',my_nodes[ii], all_nodes[my_nodes[ii]].active_scaling]); + if (all_nodes[my_nodes[ii]].active_scaling !== true) { + nodes.push(my_nodes[ii]); + } + } + for (let ii in nodes) { + current_radii[ii] = all_nodes[nodes[ii]].scale.x; + all_nodes[nodes[ii]].active_scaling = true; + } + const refreshIntervalId = setInterval(() => { + if (scale < 1) { + for (let ii in nodes) { + current_radii[ii] = all_nodes[nodes[ii]].scale.x; + all_nodes[nodes[ii]].active_scaling = false; + // console.log(['B',nodes[ii], all_nodes[nodes[ii]].active_scaling]); + } + clearInterval(refreshIntervalId); + } else { + for (let ii in nodes) { + const i = nodes[ii]; + forceLayout.all_outlines[i].scale.set(scale * current_radii[ii]); + all_nodes[i].scale.set(scale * current_radii[ii]); + } + scale = scale - scale / numsteps; + } + }, 5); + } + + toggle_legend_hover_tooltip() { + const button = d3.select('#toggle_legend_hover_tooltip_button'); + if (button.text() === 'Hide label tooltip') { + button.text('Show label tooltip'); + d3.select('#legend_hover_tooltip').remove(); + } else { + button.text('Hide label tooltip'); + + const tooltip = d3 + .select('#force_layout') + .append('div') + .attr('id', 'legend_hover_tooltip') + .style('background-color', 'rgba(100,100,100,.92)') + .style('position', 'absolute') + .style('top', '100px') + .style('left', '100px') + .style('padding', '5px') + .style('width', '200px') + .style('border-radius', '5px') + .style('visibility', 'hidden'); + + d3.select('#force_layout').on('mousemove', () => { + const name = document.getElementById('labels_menu').value; + if (name.length > 0) { + const cat_color_map = this.categorical_coloring_data[name].label_colors; + const cat_label_list = this.categorical_coloring_data[name].label_list; + + let hover_clusters = []; + const dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = d3.event.clientX - dim.left; + let y = d3.event.clientY - dim.top; + x = (x - forceLayout.sprites.position.x) / forceLayout.sprites.scale.x; + y = (y - forceLayout.sprites.position.y) / forceLayout.sprites.scale.y; + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + const rad = Math.sqrt((forceLayout.all_nodes[i].x - x) ** 2 + (forceLayout.all_nodes[i].y - y) ** 2); + if (rad < forceLayout.all_nodes[i].scale.x * 20) { + hover_clusters.push(cat_label_list[i]); + } + } + + hover_clusters = hover_clusters.filter((item, pos) => { + return hover_clusters.indexOf(item) === pos; + }); + + if (hover_clusters.length > 0) { + tooltip.style('visibility', 'visible'); + } else { + tooltip.style('visibility', 'hidden'); + } + + tooltip.selectAll('div').remove(); + tooltip + .selectAll('div') + .data(hover_clusters) + .enter() + .append('div') + .attr('class', 'legend_row') + .style('height', '25px') + .style('margin-top', '0px'); + + const widths = []; + tooltip.selectAll('div').each(function(d) { + d3.select(this) + .append('div') + .style('background-color', cat_color_map[d]); + const tt = d3 + .select(this) + .append('div') + .attr('class', 'text_label_div') + .append('p') + .text(d) + .style('float', 'left') + .style('white-space', 'nowrap') + .style('margin-top', '-6px') + .style('margin-left', '3px'); + widths.push(parseFloat(tt.style('width').split('px')[0])); + }); + + const height = parseFloat(tooltip.style('height').split('px')[0]); + tooltip + .style('width', (d3.max(widths) + 45).toString() + 'px') + .style('left', d3.event.x.toString() + 'px') + .style('top', (d3.event.y - height - 40).toString() + 'px'); + } + }); + } + } + + get_hover_cells = e => { + const dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = e.clientX - dim.left; + let y = e.clientY - dim.top; + x = (x - forceLayout.sprites.position.x) / forceLayout.sprites.scale.x; + y = (y - forceLayout.sprites.position.y) / forceLayout.sprites.scale.y; + const hover_cells = []; + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (forceLayout.all_outlines[i].selected) { + const rad = Math.sqrt((forceLayout.all_nodes[i].x - x) ** 2 + (forceLayout.all_nodes[i].y - y) ** 2); + if (rad < forceLayout.all_nodes[i].scale.x * 20) { + hover_cells.push(i); + } + } + } + return hover_cells; + }; + + max_color = c => { + return d3.max([c.r, c.b, c.g]); + }; + + min_color = c => { + return d3.min([c.r, c.b, c.g]); + }; + + average_color = c => { + return d3.mean([c.r, c.b, c.g]); + }; +} diff --git a/scripts_1_6_dev/colorbar_style.css b/src/colorbar_style.css similarity index 100% rename from scripts_1_6_dev/colorbar_style.css rename to src/colorbar_style.css diff --git a/src/colorpicker_layout.js b/src/colorpicker_layout.js new file mode 100755 index 0000000..9bc57f8 --- /dev/null +++ b/src/colorpicker_layout.js @@ -0,0 +1,207 @@ +import * as d3 from 'd3'; + +import { colorBar, forceLayout } from './main'; +import { rgbToHex } from './util'; + +let tmp_cat_coloring = null; + +(function($) { + let initLayout = function() { + let hash = window.location.hash.replace('#', ''); + $('#colorpickerHolder').ColorPicker({ flat: true }); + }; + EYE.register(initLayout, 'init'); +})(jQuery); + +export const colorpicker_submit = (hex) => { + console.log(hex); +}; + +export const colorpicker_setup = () => { + let popup = d3.select('#colorpicker_popup'); + popup.attr('current_color', ''); + popup.attr('current_nodes', ''); + popup.attr('current_label', ''); + popup.attr('current_track', ''); + popup.on('mouseup', colorpicker_update); + popup.on('mousemove', colorpicker_update); + popup.call( + d3.drag() + .on('start', colorpicker_popup_dragstarted) + .on('drag', colorpicker_popup_dragged) + .on('end', colorpicker_popup_dragended), + ); + + let button_bar = d3.select('#colorpicker_button_bar'); + button_bar + .append('button') + .text('Close') + .on('click', close_colorpicker_popup); + + button_bar + .append('button') + .text('Save') + .on('click', save_colorpicker_colors); + + button_bar + .append('button') + .text('Restore') + .on('click', restore_colorpicker); + + d3.select('#colorpickerHolder').on('mousedown', function() { + d3.event.stopPropagation(); + colorpicker_update(); + }); + + popup.append('div').attr('id', 'colorpicker_save_check'); + + function colorpicker_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + function colorpicker_popup_dragged() { + let cx = parseFloat( + d3 + .select('#colorpicker_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#colorpicker_popup') + .style('top') + .split('px')[0], + ); + d3.select('#colorpicker_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#colorpicker_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + function colorpicker_popup_dragended() { + return; + } +}; + +export const restore_colorpicker = () => { + colorBar.setNodeColors(); + let current_label = d3.select('#colorpicker_popup').attr('current_label'); + if (current_label !== '') { + let current_track = d3.select('#colorpicker_popup').attr('current_track'); + tmp_cat_coloring = Object.assign({}, colorBar.categorical_coloring_data[current_track].label_colors); + let current_color = colorBar.categorical_coloring_data[current_track].label_colors[current_label].replace('#', '0x'); + $('#colorpickerHolder').ColorPickerSetColor(current_color); + + d3.selectAll('.legend_row').each(function(d) { + if (d === current_label) { + d3.select(this) + .select('div') + .style('background-color', current_color.replace('0x', '#')); + } + }); + } +}; + +export const show_colorpicker_popup = (label) => { + if (forceLayout.mutable) { + let current_track = document.getElementById('labels_menu').value; + let current_color = colorBar.categorical_coloring_data[current_track].label_colors[label].replace('#', '0x'); + let nodes = []; + colorBar.categorical_coloring_data[current_track].label_list.forEach(function(l, i) { + if (label === l) { + nodes.push(i); + } + }); + + d3.select('#colorpicker_popup').attr('current_nodes', nodes.join(',')); + d3.select('#colorpicker_popup').attr('current_label', label); + d3.select('#colorpicker_popup').attr('current_color', current_color); + d3.select('#colorpicker_popup').attr('current_track', current_track); + + $('#colorpickerHolder').ColorPickerSetColor(current_color); + tmp_cat_coloring = Object.assign({}, colorBar.categorical_coloring_data[current_track].label_colors); + + let top = + parseFloat( + d3 + .select('body') + .style('height') + .replace('px', ''), + ) - 216; + let left = + parseFloat( + d3 + .select('body') + .style('width') + .replace('px', ''), + ) - 480; + d3.select('#colorpicker_popup').style('top', top.toString() + 'px'); + d3.select('#colorpicker_popup').style('left', left.toString() + 'px'); + d3.select('#colorpicker_popup').style('visibility', 'visible'); + } +}; + +export const close_colorpicker_popup = () => { + d3.select('#colorpicker_popup').style('visibility', 'hidden'); + d3.select('#colorpicker_popup').attr('current_nodes', ''); + d3.select('#colorpicker_popup').attr('current_label', ''); + d3.select('#colorpicker_popup').attr('current_color', ''); + d3.select('#colorpicker_popup').attr('current_track', ''); + tmp_cat_coloring = null; +}; + +export const colorpicker_update = () => { + let rgb = d3.select('.colorpicker_new_color').style('background-color'); + rgb = rgb + .replace('rgb(', '') + .replace(')', '') + .replace(',', '') + .replace(',', '') + .split(' '); + let current_color = rgbToHex(parseInt(rgb[0], 10), parseInt(rgb[1], 10), parseInt(rgb[2], 10)); + + if (current_color !== d3.select('#colorpicker_popup').attr('current_color')) { + d3.select('#colorpicker_popup') + .attr('current_nodes') + .split(',') + .forEach(function(i) { + forceLayout.all_nodes[i].tint = current_color; + }); + } + + let current_label = d3.select('#colorpicker_popup').attr('current_label'); + if (current_label !== '') { + tmp_cat_coloring[current_label] = current_color.replace('0x', '#'); + + d3.selectAll('.legend_row').each(function(d) { + if (d === current_label) { + d3.select(this) + .select('div') + .style('background-color', current_color.replace('0x', '#')); + } + }); + } +}; + +export const save_colorpicker_colors = () => { + let current_track = document.getElementById('labels_menu').value; + if (current_track === d3.select('#colorpicker_popup').attr('current_track')) { + if (tmp_cat_coloring != null) { + colorBar.categorical_coloring_data[current_track].label_colors = tmp_cat_coloring; + let text = JSON.stringify(colorBar.categorical_coloring_data); + let name = window.location.search; + let path = name.slice(1, name.length) + '/categorical_coloring_data.json'; + console.log('gothere'); + $.ajax({ + data: { path: path, content: text }, + success: function() { + d3.select('#colorpicker_save_check').style('opacity', '1'); + setTimeout(function() { + d3.select('#colorpicker_save_check') + .transition() + .duration(600) + .style('opacity', '0'); + }, 250); + }, + type: 'POST', + url: 'cgi-bin/save_data.py', + }); + } + } +}; diff --git a/src/currentDatasetsList_script.js b/src/currentDatasetsList_script.js new file mode 100755 index 0000000..45bc168 --- /dev/null +++ b/src/currentDatasetsList_script.js @@ -0,0 +1,197 @@ +import * as d3 from 'd3'; + +import { openInNewTab } from './util'; +import { forceLayout } from './main'; + +function add_list_item(project_directory, sub_directory, order) { + d3.json(project_directory + '/' + sub_directory + '/run_info.json').then(data => { + data.Date = data.Date.split(' ')[0]; + let list_item = d3 + .select('#dataset_list') + .append('li') + .style('order', order); + list_item.append('h3').text(sub_directory); + + let display_names = { + Filtered_Genes: 'Number of genes that passed filter', + Gene_let_Pctl: 'Gene letiability %ile (gene filtering)', + Min_Cells: 'Min expressing cells (gene filtering)', + Min_Counts: 'Min number of UMIs (gene filtering)', + Nodes: 'Number of cells', + Num_Force_Iter: 'Number of force layout iterations', + Num_Neighbors: 'Number of nearest neighbors', + Num_PCs: 'Number of principal components', + }; + + let info_box = list_item + .append('div') + .attr('class', 'dataset_key_info') + .style('width', '480px'); + + if ('Description' in data) { + if (data.Description != null) { + info_box + .append('tspan') + .append('text') + .text(data.Description) + .style('color', 'rgb(140,140,140)'); + } + } + let date_email = ''; + if ('Email' in data) { + if (data.Email.length > 0) { + date_email += data.Email + ' - '; + } + } + date_email += data.Date; + + info_box + .append('tspan') + .style('margin-top', '8px') + .append('text') + .text(date_email) + .style('color', 'rgb(110,110,110)') + .style('font-weight', '530'); + + let o = $(info_box.node()); + let new_height = o.offset().top - o.parent().offset().top - o.parent().scrollTop() + o.height(); + + let show_less_height = (new_height + 8).toString() + 'px'; + let show_more_height = (new_height + 170).toString() + 'px'; + list_item.style('height', show_less_height); + + info_box = list_item.append('div').attr('class', 'dataset_params'); + let keys = [ + 'Nodes', + 'Filtered_Genes', + 'Min_Cells', + 'Min_Counts', + 'Gene_let_Pctl', + 'Num_PCs', + 'Num_Neighbors', + 'Num_Force_Iter', + ]; + for (const s in keys) { + info_box + .append('tspan') + .append('text') + .text(display_names[keys[s]] + ': ') + .style('font-weight', 'normal') + .append('text') + .text(data[keys[s]]) + .style('font-weight', 'bold'); + } + + list_item + .selectAll('div') + .selectAll('tspan') + .selectAll('text') + .on('click', function() { + d3.event.stopPropagation(); + }); + + list_item + .append('text') + .attr('class', 'show_more_less_text') + .text('Show more') + .on('click', function() { + d3.event.stopPropagation(); + if (d3.select(this).text() === 'Show more') { + list_item.transition('200').style('height', show_more_height); + d3.select(this).text('Show less'); + } else { + list_item.transition('200').style('height', show_less_height); + d3.select(this).text('Show more'); + } + }); + + list_item + .append('text') + .attr('class', 'delete_button') + .text('Delete') + .on('click', function() { + d3.event.stopPropagation(); + d3.text(project_directory + '/' + sub_directory + '/mutability.txt', (text) => { + forceLayout.mutable = text; + if (forceLayout.mutable == null) { + sweetAlert( + { + icon: 'warning', + showCancelButton: true, + text: 'Do you want to delete the SPRING subplot ' + sub_directory + '?', + title: 'Are you sure?', + }, + function(isConfirm) { + console.log(isConfirm); + if (isConfirm) { + list_item + .style('z-index', '-10') + .transition() + .duration(700) + .style('margin-top', '-115px') + .each(() => { + list_item.remove(); + }); + + $.ajax({ + data: { base_dir: project_directory, sub_dir: sub_directory }, + success: function(python_data) { + console.log(python_data); + }, + type: 'POST', + url: 'cgi-bin/delete_subdirectory.py', + }); + } + }, + ); + } else { + sweetAlert( + { + icon: 'warning', + showCancelButton: false, + title: 'This subplot cannot be deleted.', + }, + function(isConfirm) { + console.log(isConfirm); + if (isConfirm) { + return; + } + }, + ); + } + }); + }); + + list_item.on('click', function() { + let my_origin = window.location.origin; + let my_pathname_split = window.location.pathname.split('/'); + let my_pathname_new = + my_pathname_split.slice(0, my_pathname_split.length - 1).join('/') + '/springViewer.html'; + let my_url_new = my_origin + my_pathname_new + '?' + project_directory + '/' + sub_directory; + console.log(my_url_new); + openInNewTab(my_url_new); + }); + }); +} + +function populate_dataset_subdirs_list(project_directory) { + const directories = project_directory.split('/'); + const title = directories[directories.length - 1]; + d3.select('#project_directory_title').text('SPRING subplots of "' + title + '"'); + + let splitURL = window.location.href.split('?')[0].split('/'); + let base_url = splitURL.slice(0, splitURL.length - 1).join('/') + '/stickyPage.html'; + d3.select('#sticky_link').attr('href', base_url + '?' + project_directory); + + $.ajax({ + data: { path: project_directory, filename: 'run_info.json' }, + success: function(output_message) { + let subdirs = output_message.split(','); + for (let i in subdirs) { + add_list_item(project_directory, subdirs[i], i + 1); + } + }, + type: 'POST', + url: 'cgi-bin/list_directories_with_filename.py', + }); +} diff --git a/scripts_1_6_dev/currentDatasetsList_style.css b/src/currentDatasetsList_style.css similarity index 100% rename from scripts_1_6_dev/currentDatasetsList_style.css rename to src/currentDatasetsList_style.css diff --git a/src/diffex_script.js b/src/diffex_script.js new file mode 100755 index 0000000..c6d91e7 --- /dev/null +++ b/src/diffex_script.js @@ -0,0 +1,460 @@ +import * as d3 from 'd3'; +import * as Spinner from 'spinner'; + +import { colorBar } from './main'; + +export const diffex_setup = () => { + let scatter_zoom = 0.2; + let scatter_size = 4; + let scatter_jitter = 0; + + let scatter_data = null; + let scatter_x = null; + let scatter_y = null; + let scatter_xAxis = null; + let scatter_yAxis = null; + let gene_list = null; + + d3.select('#diffex_panel').attr('class', 'bottom_tab'); + d3.select('#diffex_header') + .append('div') + .attr('id', 'diffex_closeopen') + .append('button') + .text('Open') + .on('click', () => { + if (d3.select('#diffex_panel').style('height') === '50px') { + open_diffex(); + } else { + close_diffex(); + } + }); + + d3.select('#diffex_header') + .append('div') + .attr('id', 'diffex_title') + .append('text') + .text('Differential expression'); + + d3.select('#diffex_header') + .append('div') + .attr('id', 'diffex_refresh_button') + .append('button') + .text('Refresh cluster selection') + .on('click', refresh_selected_clusters); + + make_diffex_spinner('diffex_infobox'); + + d3.select('#diffex_size_slider').on('input', () => { + scatter_size = this.value / 15; + quick_scatter_update(); + }); + + d3.select('#diffex_jitter_slider').on('input', () => { + scatter_jitter = parseFloat(this.value) / 50; + quick_scatter_update(); + }); + + d3.select('#diffex_zoom_slider').on('input', () => { + scatter_zoom = 5 / (parseFloat(this.value) + 5); + quick_scatter_update(); + }); + + //open_diffex(); + + function open_diffex() { + gene_list = Object.keys(colorBar.all_gene_color_array); + d3.selectAll('#diffex_panel').style('z-index', '4'); + setTimeout(() => { + d3.select('#diffex_refresh_button').style('visibility', 'visible'); + d3.select('#diffex_panel') + .selectAll('svg') + .style('visibility', 'visible'); + d3.selectAll('.diffex_legend').style('visibility', 'visible'); + d3.select('#diffex_settings_box').style('visibility', 'visible'); + }, 200); + setTimeout(() => { + if ( + d3 + .select('#diffex_panel') + .select('svg') + .node() == null + ) { + refresh_selected_clusters(); + } + }, 500); + d3.select('#diffex_closeopen') + .select('button') + .text('Close'); + d3.select('#diffex_panel') + .transition() + .duration(500) + .style('height', '380px') + .style('width', '900px') + .style('bottom', '5px'); + } + + function close_diffex() { + d3.select('#diffex_closeopen') + .select('button') + .text('Open'); + d3.select('#diffex_refresh_button').style('visibility', 'hidden'); + d3.select('#diffex_panel') + .selectAll('svg') + .style('visibility', 'hidden'); + d3.selectAll('.diffex_legend').style('visibility', 'hidden'); + d3.select('#diffex_settings_box').style('visibility', 'hidden'); + d3.select('#diffex_infobox').style('visibility', 'hidden'); + d3.select('#diffex_panel') + .transition() + .duration(500) + .style('height', '50px') + .style('width', '280px') + .style('bottom', '106px'); + + setTimeout(() => { + d3.selectAll('#diffex_panel').style('z-index', '1'); + }, 500); + } + + function refresh_selected_clusters() { + d3.select('#diffex_infobox').style('visibility', 'visible'); + d3.select('#diffex_infobox') + .select('text') + .remove(); + d3.select('#diffex_panel') + .selectAll('svg') + .remove(); + d3.selectAll('.diffex_legend') + .selectAll('div') + .remove(); + + let n_sel = d3.selectAll('.selected')[0].length; + let n_com = d3.selectAll('.compared')[0].length; + if (n_sel === 0 && n_com === 0) { + d3.select('#diffex_infobox') + .append('text') + .text('No clusters selected'); + } else { + setTimeout(() => { + d3.select('.diffex_spinner').style('visibility', 'visible'); + }, 1); + setTimeout(() => { + make_diffex_legend(); + scatter_setup(); + d3.select('.diffex_spinner').style('visibility', 'hidden'); + d3.select('#diffex_infobox').style('visibility', 'hidden'); + }, 100); + } + } + + function scatter_setup() { + let blue_selection = []; + let yellow_selection = []; + d3.selectAll('.selected').each(d => { + yellow_selection.push(d.number); + }); + d3.selectAll('.compared').each(d => { + blue_selection.push(d.number); + }); + + let xx = []; + let yy = []; + gene_list.forEach(d => { + xx.push(masked_average(colorBar.all_gene_color_array[d], blue_selection)); + yy.push(masked_average(colorBar.all_gene_color_array[d], yellow_selection)); + }); + + scatter_data = []; + xx.forEach(function(d, i) { + if ((xx[i] === 0 && yy[i] !== 0) || (xx[i] !== 0 && yy[i] === 0) || xx[i] / yy[i] > 1.5 || yy[i] / xx[i] > 1.5) { + scatter_data.push([ + xx[i], + yy[i], + ((Math.random() - 0.3) * d3.max(xx)) / 100, + ((Math.random() - 0.3) * d3.max(yy)) / 100, + gene_list[i], + ]); + } + }); + + const margin = { top: 15, right: 545, bottom: 130, left: 95 }; + const width = document.getElementById('diffex_panel').offsetWidth - margin.left - margin.right; + const height = document.getElementById('diffex_panel').offsetHeight - margin.top - margin.bottom; + + scatter_x = d3 + .scaleLinear() + .domain([ + 0, + d3.max(scatter_data, d => { + return d[0] * scatter_zoom; + }) + 0.02, + ]) + .range([0, width]); + + scatter_y = d3 + .scaleLinear() + .domain([ + 0, + d3.max(scatter_data, d => { + return d[0] * scatter_zoom; + }) + 0.02, + ]) + .range([height, 0]); + + let chart = d3 + .select('#diffex_panel') + .append('svg:svg') + .attr('width', width + 108) + .attr('height', height + margin.top + margin.bottom) + .attr('class', 'chart'); + + let main = chart + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + .attr('width', width) + .attr('height', height) + .attr('class', 'main'); + + // draw the x axis + scatter_xAxis = d3.axisBottom(scatter_x); + + main + .append('g') + .attr('transform', 'translate(0,' + height + ')') + .attr('class', 'scatter_axis') + .attr('id', 'diffex_scatter_x_axis') + .call(scatter_xAxis) + .append('text') + .attr('class', 'axis_label') + .style('fill', 'blue') + .attr('y', margin.bottom) + .attr('x', width / 2) + .style('font-size', '18px') + .attr('dy', '-4.8em') + .style('text-anchor', 'middle') + .text('Negative selection (blue)'); + + // draw the y axis + scatter_yAxis = d3.axisLeft(scatter_y); + + main + .append('g') + .attr('transform', 'translate(0,0)') + .attr('class', 'scatter_axis') + .attr('id', 'diffex_scatter_y_axis') + .call(scatter_yAxis) + .append('text') + .attr('class', 'axis_label') + .style('fill', '#999900') + .attr('transform', 'rotate(-90)') + .attr('y', -margin.left) + .attr('x', -height / 2) + .style('font-size', '18px') + .attr('dy', '2.6em') + .style('text-anchor', 'middle') + .text('Positive selection (yellow)'); + + let g = main.append('svg:g'); + + let mask_points = + scatter_x(0).toString() + + ',' + + scatter_y(0).toString() + + ' ' + + scatter_x(100).toString() + + ',' + + scatter_y(150).toString() + + ' ' + + scatter_x(150).toString() + + ',' + + scatter_y(100).toString() + + ' ' + + scatter_x(0).toString() + + ',' + + scatter_y(0).toString(); + + g.append('svg:polygon') + .attr('id', 'diffex_scatter_mask') + .style('fill', '#bdbdbd') + .attr('points', mask_points); + + g.selectAll('.diffex-scatter-dots') + .data(scatter_data) + .enter() + .append('svg:circle') + .attr('class', 'diffex-scatter-dots') + .attr('cx', (d, i) => { + return scatter_x(d[0] + d[2] * scatter_jitter); + }) + .attr('cy', d => { + return scatter_y(d[1] + d[2] * scatter_jitter); + }) + .attr('r', scatter_size); + + d3.selectAll('.diffex-scatter-dots') + .on('mouseenter', d => { + let selectedG = d[4]; + d3.select('#tooltip_gene_name') + .select('text') + .text(selectedG); + d3.select('#tooltip').style('background-color', 'green'); + let ww = d3 + .select('#tooltip_gene_name') + .node() + .getBoundingClientRect().width; + d3.select('#tooltip').style('width', (65 + ww).toString() + 'px'); + d3.select('#tooltip').style('visibility', 'visible'); + + let rect = d3 + .select('body') + .node() + .getBoundingClientRect(); + d3.select('#tooltip') + .style('bottom', rect.height - 5 - event.pageY + 'px') + .style('right', rect.width - event.pageX + 20 + 'px'); + d3.select(this).attr('r', scatter_size * 3); + }) + .on('mouseleave', () => { + d3.select('#tooltip').style('visibility', 'hidden'); + d3.select(this).attr('r', scatter_size); + }) + .on('click', d => { + d3.select('#green_menu').node().value = d[4]; + colorBar.update_slider(); + }); + } + + function masked_average(vals, indexes) { + if (indexes.length === 0) { + return d3.sum(vals) / vals.length; + } else { + let out = 0; + indexes.forEach(d => { + out = out + vals[d]; + }); + return out / indexes.length; + } + } + + function quick_scatter_update() { + scatter_x.domain([ + 0, + d3.max(scatter_data, d => { + return d[0] * scatter_zoom; + }) + 0.02, + ]); + scatter_y.domain([ + 0, + d3.max(scatter_data, d => { + return d[0] * scatter_zoom; + }) + 0.02, + ]); + d3.select('#diffex_scatter_x_axis').call(scatter_xAxis); + d3.select('#diffex_scatter_y_axis').call(scatter_yAxis); + + d3.selectAll('.diffex-scatter-dots') + .data(scatter_data) + .enter() + .append('svg:circle'); + d3.selectAll('.diffex-scatter-dots') + .attr('cx', (d, i) => { + return scatter_x(d[0] + d[2] * scatter_jitter); + }) + .attr('cy', d => { + return scatter_y(d[1] + d[3] * scatter_jitter); + }) + .attr('r', scatter_size); + } + + function make_diffex_legend() { + let yellow_list = []; + let blue_list = []; + d3.selectAll('.selected').each(d => { + yellow_list.push(d); + }); + d3.selectAll('.compared').each(d => { + blue_list.push(d); + }); + + if (yellow_list.length > 0) { + d3.select('#diffex_legend_upper') + .selectAll('.diffex_legend_row') + .data(yellow_list) + .enter() + .append('div') + .style('display', 'inline-block') + .attr('class', 'diffex_legend_row') + .style('height', '22px') + .style('margin-top', '0px') + .style('overflow', 'scroll') + .style('background-color', 'yellow'); + } else { + d3.select('#diffex_legend_upper') + .append('div') + .style('margin-top', '18px') + .append('p') + .text('All clusters'); + } + + if (blue_list.length > 0) { + d3.select('#diffex_legend_lower') + .selectAll('.diffex_legend_row') + .data(blue_list) + .enter() + .append('div') + .style('display', 'inline-block') + .attr('class', 'diffex_legend_row') + .style('height', '22px') + .style('margin-top', '0px') + .style('overflow', 'scroll') + .style('background-color', '#9999ff'); + } else { + d3.select('#diffex_legend_lower') + .append('div') + .style('margin-top', '18px') + .append('p') + .text('All clusters'); + } + + d3.selectAll('.diffex_legend_row') + .append('div') + .attr('class', 'diffex_text_label_div') + .append('p') + .text(d => { + return d.name; + }) + .style('float', 'left') + .style('white-space', 'nowrap') + .style('margin-top', '-6px') + .style('margin-left', '3px'); + } +}; + +function make_diffex_spinner(element) { + let opts = { + className: 'diffex_spinner', // The CSS class to assign to the spinner + color: 'gray', // #rgb or #rrggbb or array of colors + corners: 1, // Corner roundness (0..1) + direction: 1, // 1: clockwise, -1: counterclockwise + fps: 20, // Frames per second when using setTimeout() as a fallback for CSS + hwaccel: true, // Whether to use hardware acceleration + left: '50%', // Left position relative to parent + length: 50, // The length of each line + lines: 17, // The number of lines to draw + opacity: 0.15, // Opacity of the lines + position: 'absolute', // Element positioning + radius: 60, // The radius of the inner circle + rotate: 8, // The rotation offset + scale: 0.22, // Scales overall size of the spinner + shadow: true, // Whether to render a shadow + speed: 0.9, // Rounds per second + top: '30%', // Top position relative to parent + trail: 60, // Afterglow percentage + width: 20, // The line thickness + zIndex: 3000, // The z-index (defaults to 2000000000) + }; + let target = document.getElementById(element); + let spinner = new Spinner(opts).spin(target); + $(target).data('spinner', spinner); + d3.select('.diffex_spinner').style('visibility', 'hidden'); +} diff --git a/scripts_1_5_dev/diffex_style.css b/src/diffex_style.css similarity index 100% rename from scripts_1_5_dev/diffex_style.css rename to src/diffex_style.css diff --git a/scripts_1_6_dev/doublet_detector.css b/src/doublet_detector.css similarity index 100% rename from scripts_1_6_dev/doublet_detector.css rename to src/doublet_detector.css diff --git a/src/doublet_detector.js b/src/doublet_detector.js new file mode 100644 index 0000000..245c5bb --- /dev/null +++ b/src/doublet_detector.js @@ -0,0 +1,349 @@ +import * as d3 from 'd3'; +import { colorBar, forceLayout, cloneViewer, project_directory, graph_directory } from './main'; +import { read_csv } from './util'; + +export default class DoubletDetector { + /** @type DoubletDetector */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call DoubletDetector.create()!'); + } + return this._instance; + } + + static create() { + if (!this._instance) { + this._instance = new DoubletDetector(); + return this._instance; + } else { + throw new Error( + 'DoubletDetector.create() has already been called, get the existing instance with DoubletDetector.instance!', + ); + } + } + + constructor() { + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'doublet_popup'); + + this.button_bar = this.popup + .append('div') + .attr('id', 'doublet_button_bar') + .on('mousedown', () => { + d3.event.stopPropagation(); + }); + + this.button_bar + .append('label') + .html('k = ') + .append('input') + .attr('id', 'doublet_k_input') + .property('value', 50); + this.button_bar + .append('label') + .html('r = ') + .append('input') + .attr('id', 'doublet_r_input') + .property('value', 2); + this.button_bar + .append('label') + .html('f = ') + .append('input') + .attr('id', 'doublet_f_input') + .property('value', 0.1); + this.button_bar + .append('button') + .text('Run') + .on('click', this.run_doublet_detector); + this.button_bar + .append('button') + .text('Close') + .on('click', this.hide_doublet_popup); + + this.text_box = this.popup + .append('div') + .attr('id', 'doublet_description') + .append('text') + .html( + 'Predict mixed-celltype doublets. \ + Uses a kNN classifier to identify transcriptomes that resemble simulated doublets. \ +

k : the number neighbors used in the classifier \ +
r : the ratio of simulated doublets to observed cells \ +
f : the expected doublet rate', + ); + + this.doublet_notify_popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'doublet_notification') + .style('visibility', 'hidden'); + this.doublet_notify_popup + .append('div') + .attr('id', 'doublet_notify_text') + .append('text') + .text('Doublet detector finished! See custom colors menu.'); + + this.doublet_notify_popup + .append('button') + .text('Close') + .on('mousedown', () => this.hide_doublet_notification()); + + d3.select('#doublet_popup').call( + d3 + .drag() + .on('start', () => this.doublet_popup_dragstarted()) + .on('drag', () => this.doublet_popup_dragged()) + .on('end', () => this.doublet_popup_dragended()), + ); + } + // <-- DoubletDetector Constructor End --> + + doublet_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + doublet_popup_dragged() { + let cx = parseFloat( + d3 + .select('#doublet_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#doublet_popup') + .style('top') + .split('px')[0], + ); + d3.select('#doublet_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#doublet_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + doublet_popup_dragended() { + return; + } + + // function show_processing_mask() { + // popup.append('div').attr('id','doublet_processing_mask').append('div').append('text') + // .text('Running doublet detector... you will be notified upon completion.') + // .style('opacity', 0.0) + // .transition() + // .duration(500) + // .style('opacity', 1.0); + + // let opts = { + // lines: 17 // The number of lines to draw + // , length: 35 // The length of each line + // , width: 15 // The line thickness + // , radius: 50 // The radius of the inner circle + // , scale: 0.22 // Scales overall size of the spinner + // , corners: 1 // Corner roundness (0..1) + // , color: '#000' // #rgb or #rrggbb or array of colors + // , opacity: 0.2 // Opacity of the lines + // , rotate: 8 // The rotation offset + // , direction: 1 // 1: clockwise, -1: counterclockwise + // , speed: 0.9 // Rounds per second + // , trail: 60 // Afterglow percentage + // , fps: 20 // Frames per second when using setTimeout() as a fallback for CSS + // , zIndex: 2e9 // The z-index (defaults to 2000000000) + // , className: 'spinner' // The CSS class to assign to the spinner + // , top: '50%' // Top position relative to parent + // , left: '50%' // Left position relative to parent + // , shadow: false // Whether to render a shadow + // , hwaccel: true // Whether to use hardware acceleration + // , position: 'relative' // Element positioning + // } + // let target = document.getElementById('doublet_processing_mask'); + // let spinner = new Spinner(opts).spin(target); + // $(target).data('spinner', spinner); + + // } + + // function hide_processing_mask() { + // // $(".spinner").remove(); + // $("#doublet_processing_mask").remove(); + + // } + + // function hide_doublet_popup_slowly() { + // d3.select("#doublet_popup").transition() + // .duration(2000) + // .transition() + // .duration(500) + // .style('opacity', 0.0) + // .each("end", function() { + // d3.select("#doublet_popup").style('visibility', 'hidden'); + // d3.select("#doublet_popup").style('opacity', '1.0'); + // }); + + // } + + // function show_doublet_notification() { + + // let mywidth = parseInt(d3.select("#doublet_notification").style("width").split("px")[0]) + // let svg_width = parseInt(d3.select("svg").style("width").split("px")[0]) + + // d3.select("#doublet_notification") + // .style("left",(svg_width/2-mywidth/2).toString()+"px") + // .style("top","0px") + // .style('opacity', 0.0) + // .style('visibility','visible') + // .transition() + // .duration(1500) + // .style('opacity', 1.0) + // .transition() + // .duration(2000) + // .transition() + // .duration(1500) + // .style('opacity', 0.0) + // .each("end", function() { + // d3.select("#doublet_notification").style('visibility', 'hidden'); + // }); + + // //d3.select("#doublet_notification").transition(1500).style('visibility','hidden'); + // } + + // <-- DoubletDetector Constructor End --> + run_doublet_detector() { + if (forceLayout.mutable) { + var t0 = new Date(); + var k = $('#doublet_k_input').val(); + var r = $('#doublet_r_input').val(); + var f = $('#doublet_f_input').val(); + + // show_processing_mask(); + // hide_doublet_popup_slowly(); + + d3.select('#doublet_notification') + .select('text') + .text('Running doublet detector... you will be notified upon completion.'); + this.show_doublet_notification(); + this.hide_doublet_popup(); + + console.log(k, r, f); + $.ajax({ + data: { + base_dir: graph_directory, + sub_dir: project_directory, + k: k, + r: r, + f: f, + }, + success: data => { + let t1 = new Date(); + console.log('Ran doublet detector: ', t1.getTime() - t0.getTime()); + d3.select('#doublet_notification') + .select('text') + .text('Doublet detector finished! See Custom Colors menu.'); + this.show_doublet_notification(); + if (d3.select('#clone_viewer_popup').style('visibility') === 'visible') { + $('#clone_viewer_popup').remove(); + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + cloneViewer.node_status[i].source = false; + cloneViewer.node_status[i].target = false; + } + for (let i in cloneViewer.clone_nodes) { + cloneViewer.deactivate_nodes(i); + } + for (let i in cloneViewer.clone_edges) { + cloneViewer.deactivate_edges(i); + } + cloneViewer.targetCircle.clear(); + + cloneViewer.start_clone_viewer(); + } else { + $('#clone_viewer_popup').remove(); + } + + // hide_processing_mask(); + // open json file containing gene sets and populate drop down menu + let noCache = new Date().getTime(); + d3.json(project_directory + '/color_stats.json' + '?_=' + noCache).then(colorData => { + colorBar.color_stats = colorData; + }); + d3.text(project_directory + '/color_data_gene_sets.csv' + '?_=' + noCache).then(text => { + colorBar.gene_set_color_array = read_csv(text); + colorBar.dispatch.call('load', this, colorBar.gene_set_color_array, 'gene_sets'); + colorBar.update_slider(); + console.log('loaded doublet scores into menu'); + }); + }, + type: 'POST', + url: 'cgi-bin/run_doublet_detector.py', + }); + } else { + d3.select('#doublet_notification') + .select('text') + .text('Sorry, this dataset cannot be edited.'); + this.show_doublet_notification(); + } + } + + show_doublet_notification() { + let mywidth = parseInt( + d3 + .select('#doublet_notification') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + + d3.select('#doublet_notification') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '0px') + .style('opacity', 0.0) + .style('visibility', 'visible') + .transition() + .duration(500) + .style('opacity', 1.0); + } + + hide_doublet_notification() { + d3.select('#doublet_notification') + .style('opacity', 0.0) + .style('visibility', 'hidden'); + } + + show_doublet_popup() { + if (forceLayout.mutable) { + let mywidth = parseInt( + d3 + .select('#doublet_popup') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#doublet_popup') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '80px') + .style('visibility', 'visible'); + } else { + d3.select('#doublet_notification') + .select('text') + .text('Sorry, this dataset cannot be edited.'); + this.show_doublet_notification(); + } + } + + hide_doublet_popup() { + d3.select('#doublet_popup').style('visibility', 'hidden'); + } +} diff --git a/src/downloadSelectedExpr_script.js b/src/downloadSelectedExpr_script.js new file mode 100644 index 0000000..8d57276 --- /dev/null +++ b/src/downloadSelectedExpr_script.js @@ -0,0 +1,220 @@ +import * as d3 from 'd3'; +import { forceLayout, graph_directory, sub_directory } from './main'; + +export default class DownloadSelectedExpr { + /** @type DownloadSelectedExpr */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call DownloadSelectedExpr.create()!'); + } + return this._instance; + } + + static create(project_directory, color_menu_genes) { + if (!this._instance) { + this._instance = new DownloadSelectedExpr(); + return this._instance; + } else { + throw new Error( + 'DownloadSelectedExpr.create() has already been called, get the existing instance with DownloadSelectedExpr.instance!', + ); + } + } + + constructor() { + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'downloadSelectedExpr_popup'); + + this.button_bar = this.popup + .append('div') + .attr('id', 'downloadSelectedExpr_button_bar') + .style('width', '100%'); + + this.button_bar.append('text').text('Download raw data for selected cells'); + + this.close_button = this.button_bar + .append('button') + .text('Close') + .on('mousedown', () => this.hide_downloadSelectedExpr_popup()); + + this.popup + .append('div') + .attr('class', 'downloadSelectedExpr_input_div') + .append('label') + .text('Cell subset name') + .append('input') + .attr('type', 'text') + .attr('id', 'input_subset_name_download') + .attr('value', 'e.g. "My_favorite_cells"') + .style('width', '220px'); + + this.popup + .append('div') + .attr('class', 'downloadSelectedExpr_input_div') + .append('label') + .text('Email address') + .append('input') + .attr('type', 'text') + .attr('id', 'input_email_download') + .style('width', '220px'); + + // popup.append('div').attr('class','downloadSelectedExpr_input_div') + // .append('label').text('Save force layout animation') + // .append('button').text('No') + // .attr('id','input_animation') + // .on('click', function() { + // if (d3.select(this).text()=='Yes') { d3.select(this).text('No'); } + // else { d3.select(this).text('Yes'); } + // }); + + this.popup + .append('div') + .attr('id', 'downloadSelectedExpr_submission_div') + .attr('class', 'downloadSelectedExpr_input_div') + .append('button') + .text('Submit') + .on('click', () => this.downloadSelectedExpr()); + + this.popup + .append('div') + .attr('id', 'downloadSelectedExpr_message_div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .style('overflow', 'scroll') + .append('text'); + + d3.selectAll('.downloadSelectedExpr_input_div').on('mousedown', () => { + d3.event.stopPropagation(); + }); + + d3.select('#downloadSelectedExpr_popup').call( + d3 + .drag() + .on('start', () => this.downloadSelectedExpr_popup_dragstarted()) + .on('drag', () => this.downloadSelectedExpr_popup_dragged()) + .on('end', () => this.downloadSelectedExpr_popup_dragended()), + ); + } + // <-- DownloadSelectedExpr Constructor End --> + + downloadSelectedExpr_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + downloadSelectedExpr_popup_dragged() { + let cx = parseFloat( + d3 + .select('#downloadSelectedExpr_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#downloadSelectedExpr_popup') + .style('top') + .split('px')[0], + ); + d3.select('#downloadSelectedExpr_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#downloadSelectedExpr_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + downloadSelectedExpr_popup_dragended() { + return; + } + + hide_downloadSelectedExpr_popup() { + d3.select('#downloadSelectedExpr_popup') + .style('visibility', 'hidden') + .style('height', '200px'); + } + + show_downloadSelectedExpr_popup = () => { + let mywidth = parseInt( + d3 + .select('#downloadSelectedExpr_popup') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#input_description').style('height', '22px'); + d3.select('#downloadSelectedExpr_message_div') + .style('visibility', 'hidden') + .style('height', '0px'); + d3.select('#input_description').node().value = ''; + d3.select('#downloadSelectedExpr_popup') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '10px') + .style('padding-bottom', '0px') + .style('visibility', 'visible'); //.style('height','300px'); + }; + downloadSelectedExpr() { + let sel2text = ''; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + sel2text = sel2text + ',' + i.toString(); + } + } + if (sel2text.length > 0) { + sel2text = sel2text.slice(1, sel2text.length); + } + let t0 = new Date(); + let my_origin = window.location.origin; + let my_pathname = window.location.pathname; + let my_pathname_split = my_pathname.split('/'); + let my_pathname_start = my_pathname_split.slice(0, my_pathname_split.length - 1).join('/'); + + let subset_name = $('#input_subset_name_download').val(); + let user_email = $('#input_email_download').val(); + + let output_message = 'Checking input...
'; + + d3.select('#downloadSelectedExpr_popup') + .transition() + .duration(200) + .style('height', '375px'); + + d3.select('#downloadSelectedExpr_message_div') + .transition() + .duration(200) + .style('height', '120px') + .each(() => { + d3.select('#downloadSelectedExpr_message_div').style('visibility', 'inherit'); + d3.select('#downloadSelectedExpr_message_div') + .select('text') + .html(output_message); + }); + + console.log('Downloading expression'); + $.ajax({ + data: { + base_dir: graph_directory, + current_dir: graph_directory + '/' + sub_directory, + email: user_email, + my_origin: my_origin + my_pathname_start, + selected_cells: sel2text, + selection_name: subset_name, + }, + success: data => { + let t1 = new Date(); + //console.log(data); + d3.select('#downloadSelectedExpr_message_div') + .select('text') + .html(data); + }, + type: 'POST', + url: 'cgi-bin/download_expression.submit.py', + }); + } +} diff --git a/scripts_1_6_dev/downloadSelectedExpr_style.css b/src/downloadSelectedExpr_style.css similarity index 100% rename from scripts_1_6_dev/downloadSelectedExpr_style.css rename to src/downloadSelectedExpr_style.css diff --git a/src/file_helper.js b/src/file_helper.js new file mode 100644 index 0000000..35dee38 --- /dev/null +++ b/src/file_helper.js @@ -0,0 +1,41 @@ +import * as d3 from 'd3'; +import { project_directory } from './main'; + +export const getData = async (name, fileType) => { + if (window.cacheData && window.cacheData.has(name)) { + console.log(`I have cached data for ${name}!`); + return window.cacheData.get(name); + } + + try { + const filePath = `${project_directory}/${name}.${fileType}`; + const file = await fetchFile(filePath, fileType); + return file; + } catch (e) { + try { + const filePath = `${project_directory}/../${name}.${fileType}`; + const file = await fetchFile(filePath, fileType); + return file; + } catch (e) { + console.log(e); + return ''; + } + } +}; + +const fetchFile = async (filePath, fileType) => { + switch (fileType) { + case 'txt': + case 'csv': + return d3.text(filePath); + case 'json': + return d3.json(filePath); + default: { + return Promise.reject(`Sorry, currently no support for fileType '${fileType}'!`); + } + } +} + +export const addData = (key, value) => { + window.cacheData.set(key, value); +}; diff --git a/src/forceLayout_script.js b/src/forceLayout_script.js new file mode 100755 index 0000000..f0fc7e8 --- /dev/null +++ b/src/forceLayout_script.js @@ -0,0 +1,958 @@ +import * as d3 from 'd3'; + +import { LineSprite } from './LineSprite'; +import { + cloneViewer, + cluster2, + colorBar, + downloadSelectedExpr, + paga, + project_directory, + selectionLogic, + selectionScript, + smoothingImputation, + springPlot, + sub_directory, +} from './main'; +import { SPRITE_IMG_WIDTH, downloadFile, rgbToHex } from './util'; +import { collapse_settings } from './settings_script'; +import { rotation_update } from './rotation_script'; +import { getData } from './file_helper'; + +export default class ForceLayout { + /** @type ForceLayout */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call ForceLayout.create()!'); + } + return this._instance; + } + + static async create() { + if (!this._instance) { + this._instance = new ForceLayout(); + await this._instance.loadData(); + return this._instance; + } else { + throw new Error( + 'ForceLayout.create() has already been called, get the existing instance with ForceLayout.instance!', + ); + } + } + + constructor() { + this.width = window.innerWidth - 15; + this.height = window.innerHeight - 70; + + this.all_edge_ends = new Array(); + this.all_edges = new Array(); + this.all_nodes = []; + this.all_outlines = []; + this.app = new PIXI.Application(this.width, this.height, { backgroundColor: 0xdcdcdc }); + this.base_colors = new Array(); + this.being_dragged = false; + this.coordinates = new Array(); + this.edge_container = new PIXI.Container(); + this.force_on = 1; + this.mutable = true; + this.sprites = new PIXI.Container(); + this.stashed_coordinates = new Array(); + this.svg_graph = d3.select(null); + this.xScale = d3.scaleLinear(); + this.yScale = d3.scaleLinear(); + this.zoomer = d3.zoom(); + + d3.select('#toggleforce') + .select('button') + .on('click', () => this.toggleForce()); + d3.select('#sound_toggle').style('visibility', 'hidden'); + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + let snd = new Audio('src/sound_effects/opennew_sound.wav'); + snd.play(); + } + + this.keyCode = 0; + this.nodeGraph = null; + + this.svg = d3 + .select('#force_layout') + .attr('tabindex', 1) + .each((d, i, nodes) => { + nodes[i].focus(); + }) + .append('svg') + .attr('id', 'force_svg') + .attr('width', this.width) + .attr('height', this.height) + .style('background', 'rgba(0,0,0,0)') + .style('position', 'absolute') + .style('top', '0px'); + + this.zoomer = d3 + .zoom() + .scaleExtent([0.02, 10]) + .on('zoom', () => this.redraw()); + ///////////////////////////////////////////////////////////////////// + + this.svg_graph = this.svg + .append('svg:g') + .call(this.zoomer) + .attr('id', 'svg_graph'); + + d3.select('#force_svg') + .append('g') + .attr('id', 'vis'); + + this.rect = this.svg_graph + .append('svg:rect') + .attr('width', this.width) //*1000) + .attr('height', this.height) //*1000) + //.attr('x',-width*500) + //.attr('y',-height*500) + .attr('fill', 'transparent') + .attr('stroke', 'transparent') + .attr('stroke-width', 1) + .attr('id', 'zrect'); + + /////////////////////// + this.svg_width = parseInt(d3.select('svg').attr('width'), 10); + + this.more_settings_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'other_frills') + .attr('id', 'show_edges_rect') + .attr('x', this.svg_width - 177) + .attr('y', 104) + .attr('fill', 'black') + .attr('fill-opacity', 0.25) + .attr('width', 200) + .attr('height', 46); + + d3.select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'other_frills') + .attr('id', 'edge_text') + .attr('y', 122) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'white') + .append('tspan') + .attr('id', 'hide_edges_tspan') + .attr('x', this.svg_width - 167) + .attr('dy', 0) + .text('Hide edges') + .append('tspan') + .attr('id', 'hide_edges_sub_tspan') + .attr('x', this.svg_width - 167) + .attr('dy', 17) + .text('(runs faster)'); + + this.imgs = d3 + .select('svg') + .selectAll('img') + .data([0]); + this.edge_toggle_image = this.imgs + .enter() + .append('svg:image') + .attr('id', 'edge_toggle_image') + .attr('xlink:href', 'stuff/check-mark.svg') + .attr('x', this.svg_width - 70) + .attr('y', 115) + .attr('width', 25) + .attr('height', 25) + .attr('class', 'other_frills') + .on('click', () => this.toggle_edges()); + + d3.select('#toggle_edges_layout').on('click', () => this.toggle_edges()); + return this; + } + // <-- ForceLayout Constructor End --> + + async loadData() { + const filePath = project_directory + '/mutability.txt'; + try { + const mutableText = await d3.text(filePath); + if (mutableText === null) { + this.mutable = true; + } else { + this.mutable = false; + } + } catch (err) { + console.log(`Unable to get mutability.txt at ${filePath}\n${err}`); + } + + // Read coordinates file if it exists + const text = await getData('coordinates', 'txt'); + if (!text.push) { + text.split('\n').forEach((entry, index, array) => { + let items = entry.split(','); + if (items.length > 1) { + let xx = parseFloat($.trim(items[1])); + let yy = parseFloat($.trim(items[2])); + let nn = parseInt($.trim(items[0]), 10); + this.coordinates.push([xx, yy]); + } + }); + } else { + this.coordinates = text; + } + document.getElementById('pixi_canvas_holder').appendChild(this.app.view); + + this.sprites.interactive = true; + this.sprites.interactiveChildren = true; + + // create an array to store all the sprites + let totalSprites = this.app.renderer instanceof PIXI.WebGLRenderer ? this.coordinates.length : 100; + + this.create_sprites(totalSprites); + + let stashed_coordinates = [{}]; + for (let i in this.all_nodes) { + stashed_coordinates[0][i] = [this.all_nodes[i].x, this.all_nodes[i].y]; + } + + this.svg_graph.call( + d3 + .drag() + .on('start', () => this.dragstarted()) + .on('drag', () => this.dragged()) + .on('end', () => this.dragended()), + ); + + await this.load_edges(this.all_nodes, this.sprites); + } + + create_sprites(totalSprites) { + for (let i = 0; i < totalSprites; i++) { + let dude = PIXI.Sprite.fromImage('stuff/disc.png'); + dude.anchor.set(0.5); + dude.scale.set((0.5 * 32) / SPRITE_IMG_WIDTH); + dude.x = this.coordinates[i][0]; + dude.y = this.coordinates[i][1]; + dude.tint = parseInt(rgbToHex(0, 0, 0), 16); + dude.alpha = 1; + dude.interactive = true; + dude.index = i; + dude.bump = 0; + dude.beingDragged = false; + this.sprites.addChild(dude); + this.all_nodes.push(dude); + this.base_colors.push({ r: 0, g: 0, b: 0 }); + + let outline = PIXI.Sprite.fromImage('stuff/annulus.png'); + outline.anchor.set(0.5); + outline.scale.set(0.5); + outline.x = this.coordinates[i][0]; + outline.y = this.coordinates[i][1]; + outline.tint = 0xffff00; + outline.index = i; + outline.bump = 0.0001; + outline.alpha = 0; + outline.selected = false; + outline.compared = false; + this.sprites.addChild(outline); + this.all_outlines.push(outline); + } + } + + load_edges = async (all_nodes, sprites) => { + this.edge_container.position = sprites.position; + this.edge_container.scale = sprites.scale; + this.edge_container.alpha = 0.5; + this.neighbors = {}; + for (let i = 0; i < all_nodes.length; i++) { + this.neighbors[i] = []; + } + try { + const edgesText = await d3.text(project_directory + '/edges.csv'); + edgesText.split('\n').forEach((entry, index) => { + if (entry.length > 0) { + let items = entry.split(';'); + let source = parseInt(items[0], 10); + let target = parseInt(items[1], 10); + + this.neighbors[source].push(target); + this.neighbors[target].push(source); + + let x1 = all_nodes[source].x; + let y1 = all_nodes[source].y; + let x2 = all_nodes[target].x; + let y2 = all_nodes[target].y; + + let color = 6579301; + let s = new LineSprite(4, color, x1, y1, x2, y2); + s._color = color; + this.edge_container.addChild(s); + this.all_edges.push(s); + this.all_edge_ends.push({ source: source, target: target }); + } + }); + } catch (e) { + console.log(`Error setting up edges: ${e}`); + } finally { + this.app.stage.addChild(this.edge_container); + this.app.stage.addChild(sprites); + } + }; + + dragstarted() { + if (selectionScript.selection_mode === 'drag_pan_zoom') { + let dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = d3.event.sourceEvent.clientX - dim.left; + let y = d3.event.sourceEvent.clientY - dim.top; + x = (x - this.sprites.position.x) / this.sprites.scale.x; + y = (y - this.sprites.position.y) / this.sprites.scale.y; + let clicked_pos_sel = false; + let clicked_neg_sel = false; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].selected) { + let rad = Math.sqrt((this.all_nodes[i].x - x) ** 2 + (this.all_nodes[i].y - y) ** 2); + if (rad < this.all_nodes[i].scale.x * 20) { + clicked_pos_sel = true; + } + } + if (this.all_outlines[i].compared) { + const newRad = Math.sqrt((this.all_nodes[i].x - x) ** 2 + (this.all_nodes[i].y - y) ** 2); + if (newRad < this.all_nodes[i].scale.x * 20) { + clicked_neg_sel = true; + } + } + } + if (clicked_pos_sel || clicked_neg_sel) { + let stash_i = this.stashed_coordinates.length; + this.stashed_coordinates.push({}); + for (let i in this.all_nodes) { + this.stashed_coordinates[stash_i][i] = [this.all_nodes[i].x, this.all_nodes[i].y]; + } + } + if (clicked_pos_sel) { + this.being_dragged = true; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].selected) { + this.all_nodes[i].beingDragged = true; + } + } + } + if (clicked_neg_sel) { + this.being_dragged = true; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].compared) { + this.all_nodes[i].beingDragged = true; + } + } + } + } + } + + dragged() { + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_nodes[i].beingDragged) { + this.all_nodes[i].x += d3.event.dx / this.sprites.scale.x; + this.all_nodes[i].y += d3.event.dy / this.sprites.scale.y; + this.all_outlines[i].x += d3.event.dx / this.sprites.scale.x; + this.all_outlines[i].y += d3.event.dy / this.sprites.scale.y; + } + } + for (let i = 0; i < this.all_edges.length; i++) { + if ( + this.all_nodes[this.all_edge_ends[i].source].beingDragged || + this.all_nodes[this.all_edge_ends[i].target].beingDragged + ) { + this.all_edges[i].x1 = this.all_nodes[this.all_edge_ends[i].source].x; + this.all_edges[i].y1 = this.all_nodes[this.all_edge_ends[i].source].y; + this.all_edges[i].x2 = this.all_nodes[this.all_edge_ends[i].target].x; + this.all_edges[i].y2 = this.all_nodes[this.all_edge_ends[i].target].y; + this.all_edges[i].updatePosition(); + } + } + } + + dragended() { + this.being_dragged = false; + for (let i = 0; i < this.all_nodes.length; i++) { + this.all_nodes[i].beingDragged = false; + } + } + + toggle_edges() { + if (this.edge_container.visible === true) { + this.edge_toggle_image.attr('xlink:href', 'stuff/ex-mark.svg'); + this.edge_container.visible = false; + d3.select('#edge_text') + .selectAll('tspan') + .remove(); + d3.select('#edge_text') + .append('tspan') + .attr('id', 'hide_edges_tspan') + .attr('x', this.svg_width - 167) + .attr('dy', 0) + .text('Show edges') + .append('tspan') + .attr('id', 'hide_edges_sub_tspan') + .attr('x', this.svg_width - 167) + .attr('dy', 17) + .text('(runs slower)'); + d3.select('#toggle_edges_layout').text('Show edges'); + } else { + this.edge_toggle_image.attr('xlink:href', 'stuff/check-mark.svg'); + this.edge_container.visible = true; + d3.select('#edge_text') + .selectAll('tspan') + .remove(); + d3.select('#edge_text') + .append('tspan') + .attr('id', 'hide_edges_tspan') + .attr('x', this.svg_width - 167) + .attr('dy', 0) + .text('Hide edges') + .append('tspan') + .attr('id', 'hide_edges_sub_tspan') + .attr('x', this.svg_width - 167) + .attr('dy', 17) + .text('(runs faster)'); + d3.select('#toggle_edges_layout').text('Hide edges'); + } + } + + move_selection_aside(side) { + // find left and right most edge of selected and non selected cells + let sel_x = []; + let non_x = []; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].selected) { + sel_x.push(this.all_nodes[i].x); + } else { + non_x.push(this.all_nodes[i].x); + } + } + let new_coordinates = {}; + let offset = 0; + if (side === 'left') { + offset = d3.min(non_x) - d3.max(sel_x) - 5; + } else { + offset = d3.max(non_x) - d3.min(sel_x) + 5; + } + + const next_frame = (steps, current_frame) => { + current_frame += 1; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].selected) { + let y = this.all_nodes[i].y; + let x = this.all_nodes[i].x + offset / steps; + this.move_node(i, x, y); + } + } + if (current_frame < steps) { + setTimeout(() => { + next_frame(steps, current_frame); + }, 2); + } else { + this.center_view(false); + this.adjust_edges(); + if (d3.select('#edge_toggle_image').attr('xlink:href') === 'stuff/check-mark.svg') { + this.blend_edges(); + } + } + }; + + this.edge_container.visible = false; + next_frame(6, -1); + } + + revert_positions = () => { + let stash_i = this.stashed_coordinates.length - 1; + for (let i in this.stashed_coordinates [stash_i]) { + this.move_node(i, this.stashed_coordinates[stash_i][i][0], this.stashed_coordinates[stash_i][i][1]); + } + this.adjust_edges(); + this.stashed_coordinates = this.stashed_coordinates.slice(0, this.stashed_coordinates.length - 1); + }; + + move_node = (i, x, y) => { + this.all_nodes[i].x = x; + this.all_nodes[i].y = y; + this.all_outlines[i].x = x; + this.all_outlines[i].y = y; + }; + + adjust_edges = () => { + for (let i in this.all_edges) { + this.all_edges[i].x1 = this.all_nodes[this.all_edge_ends[i].source].x; + this.all_edges[i].y1 = this.all_nodes[this.all_edge_ends[i].source].y; + this.all_edges[i].x2 = this.all_nodes[this.all_edge_ends[i].target].x; + this.all_edges[i].y2 = this.all_nodes[this.all_edge_ends[i].target].y; + this.all_edges[i].updatePosition(); + } + }; + + animation = () => { + // check if animation exists. if so, hide sprites and load it + const filePath = project_directory + '/animation.txt'; + d3.text(filePath) + .then(data => { + let animation_frames = []; + data.split('\n').forEach(line => { + if (line.length > 0) { + let aframe = []; + let xx = line.split(';')[0].split(','); + let yy = line.split(';')[1].split(','); + for (let i in xx) { + aframe.push([parseFloat(xx[i]), parseFloat(yy[i])]); + } + animation_frames.push(aframe); + } + }); + let any_diff = false; + for (let i = 0; i < this.coordinates.length; i++) { + if (Math.abs(this.coordinates[i][0] - animation_frames[animation_frames.length - 1][i][0]) > 5) { + any_diff = true; + } + if (Math.abs(this.coordinates[i][1] - animation_frames[animation_frames.length - 1][i][1]) > 5) { + any_diff = true; + } + } + + this.sprites.visible = true; + + const next_frame_anim = current_frame => { + current_frame += 1; + let tmp_coordinates = animation_frames[current_frame]; + + for (let i = 0; i < this.all_nodes.length; i++) { + this.all_nodes[i].x = tmp_coordinates[i][0]; + this.all_nodes[i].y = tmp_coordinates[i][1]; + this.all_outlines[i].x = tmp_coordinates[i][0]; + this.all_outlines[i].y = tmp_coordinates[i][1]; + } + + if (current_frame + 1 < animation_frames.length) { + setTimeout(() => { + next_frame_anim(current_frame); + }, 1); + } else { + next_frame_interp(-1, 10); + } + }; + + const next_frame_interp = (current_frame, steps) => { + current_frame += 1; + if (current_frame + 1 > steps || !any_diff) { + this.blend_edges(); + } else { + let last_frame = animation_frames[animation_frames.length - 1]; + for (let i = 0; i < this.all_nodes.length; i++) { + this.all_nodes[i].x += (this.coordinates[i][0] - last_frame[i][0]) / steps; + this.all_nodes[i].y += (this.coordinates[i][1] - last_frame[i][1]) / steps; + this.all_outlines[i].x += (this.coordinates[i][0] - last_frame[i][0]) / steps; + this.all_outlines[i].y += (this.coordinates[i][1] - last_frame[i][1]) / steps; + } + setTimeout(() => { + next_frame_interp(current_frame, steps); + }, 1); + } + }; + + next_frame_anim(-1); + }) + .catch(err => { + this.sprites.visible = true; + this.edge_container.visible = true; + }); + }; + + blend_edges = () => { + this.edge_container.alpha = 0; + this.edge_container.visible = true; + + const next_frame = (current_frame, min, max, steps) => { + current_frame += 1; + let alpha = (current_frame * (max - min)) / steps + min; + this.edge_container.alpha = alpha; + if (alpha < max) { + setTimeout(() => { + next_frame(current_frame, min, max, steps); + }, 5); + } + }; + + next_frame(-1, 0, 0.5, 10); + }; + + toggleForce = () => { + if (this.force_on === 1) { + d3.select('#toggleforce') + .select('button') + .text('Resume'); + this.force_on = 0; + this.force.stop(); + } else { + d3.select('#toggleforce') + .select('button') + .text('Pause'); + this.force_on = 1; + if (this.force) { + this.force = d3.forceSimulation(); + } + this.force.tick(); + } + }; + + hideAccessories = () => { + d3.selectAll('.other_frills').style('visibility', 'hidden'); + d3.selectAll('.selection_option').style('visibility', 'hidden'); + d3.selectAll('.colorbar_item').style('visibility', 'hidden'); + d3.select('svg').style('background-color', 'white'); + }; + + showAccessories = () => { + d3.selectAll('.other_frills').style('visibility', 'visible'); + d3.selectAll('.selection_option').style('visibility', 'visible'); + d3.selectAll('.colorbar_item').style('visibility', 'visible'); + d3.select('svg').style('background-color', '#D6D6D6'); + }; + + downloadSelection = () => { + let name = window.location.search; + let cell_filter_filename = window.location.search.slice(1, name.length) + '/cell_filter.txt'; + d3.text(cell_filter_filename).then(cellText => { + let cell_nums = cellText.split('\n'); + let text = ''; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].selected) { + text = text + i.toString() + ',' + cell_nums[i] + '\n'; + } + } + downloadFile(text, 'selected_cells.txt'); + }); + }; + + downloadCoordinates = () => { + let text = ''; + for (let i = 0; i < this.all_nodes.length; i++) { + text += i.toString() + ',' + this.all_nodes[i].x.toString() + ',' + this.all_nodes[i].y.toString() + '\n'; + } + downloadFile(text, 'coordinates.txt'); + }; + + initiateButtons = () => { + d3.select('#help').on('click', () => { + let win = window.open('helppage.html', '_blank'); + win.focus(); + }); + + d3.select('#center_view').on('click', () => { + this.center_view(true); + }); + + d3.select('#revert_positions').on('click', () => this.revert_positions()); + + d3.select('#move_left').on('click', () => { + this.move_selection_aside('left'); + }); + d3.select('#move_right').on('click', () => { + this.move_selection_aside('right'); + }); + + d3.select('#save_coords') + .select('button') + .on('click', () => { + if (this.mutable) { + let text = ''; + d3.select('.node') + .selectAll('circle') + .each(d => { + text = text + d.number + ',' + d.x.toString() + ',' + d.y.toString() + '\n'; + }); + let name = window.location.search; + let path = + 'coordinates/' + name.slice(9, name.length).split('/')[1] + '_coordinates.' + sub_directory + '.txt'; + $.ajax({ + data: { path: path, content: text }, + type: 'POST', + url: 'cgi-bin/save_data.py', + }); + } + }); + + d3.select('#download_png') + .on('click', () => this.download_png()) + .on('mouseenter', () => { + d3.select('#container') + .append('div') + .attr('id', 'screenshot_tooltip') + .style('position', 'absolute') + .style('padding-top', '8px') + .style('padding-bottom', '8px') + .style('padding-left', '10px') + .style('padding-right', '10px') + .style('width', '150px') + .style( + 'left', + ( + parseInt( + d3 + .select('#download_dropdown') + .style('left') + .split('px')[0], + 10, + ) - 8 + ).toString() + 'px', + ) + .style('top', d3.select('#download_dropdown').style('height')) + .style('background-color', 'rgba(0,0,0,.4)') + .append('p') + .text('Zoom in on plot for higher resolution download') + .style('margin', '0px') + .style('color', 'white') + .style('font-family', 'sans-serif') + .style('font-size', '13px'); + }) + .on('mouseleave', () => { + d3.select('#screenshot_tooltip').remove(); + }); + + d3.select('#rotation_update').on('click', () => rotation_update()); + + d3.select('#download_coordinates').on('click', () => this.downloadCoordinates()); + + d3.select('#download_selection').on('click', () => this.downloadSelection()); + + d3.select('#show_download__selected_expr_popup').on('click', () => + downloadSelectedExpr.show_downloadSelectedExpr_popup(), + ); + + d3.select('#show_make_new_SPRINGplot_popup').on('click', () => springPlot.show_make_new_SPRINGplot_popup()); + + d3.select('#start_clone_viewer').on('click', () => { + cloneViewer.start_clone_viewer(); + }); + + d3.select('#show_imputation_popup').on('click', () => smoothingImputation.show_imputation_popup()); + d3.select('#show_selection_logic_popup').on('click', () => selectionLogic.show_selection_logic_popup()); + d3.select('#show_doublet_popup').on('click', () => springPlot.show_make_new_SPRINGplot_popup()); + d3.select('#run_clustering').on('click', () => cluster2.run_clustering()); + d3.select('#show_PAGA_popup').on('click', () => paga.show_PAGA_popup()); + d3.select('#toggle_legend_hover_tooltip_button').on('click', () => colorBar.toggle_legend_hover_tooltip()); + d3.select('#extend_selection').on('click', () => selectionScript.extend_selection()); + }; + + download_png = () => { + let searchPaths = window.location.search.split('/'); + const path = searchPaths[searchPaths.length - 2] + '_' + searchPaths[searchPaths.length - 1] + '.png'; + this.download_sprite_as_png(this.app.renderer, this.app.stage, path); + }; + + download_sprite_as_png = (renderer, sprite, fileName) => { + renderer.extract.canvas(sprite).toBlob(b => { + let a = document.createElement('a'); + document.body.appendChild(a); + a.download = fileName; + a.href = URL.createObjectURL(b); + a.click(); + a.remove(); + }, 'image/png'); + }; + + showToolsDropdown() { + if (d3.select('#tools_dropdown').style('height') === 'auto') { + this.closeDropdown(); + collapse_settings(); + setTimeout(() => { + document.getElementById('tools_dropdown').classList.toggle('show'); + }, 10); + } + } + + showDownloadDropdown() { + if (d3.select('#download_dropdown').style('height') === 'auto') { + this.closeDropdown(); + collapse_settings(); + setTimeout(() => { + document.getElementById('download_dropdown').classList.toggle('show'); + }, 10); + } + } + + showLayoutDropdown() { + if (d3.select('#layout_dropdown').style('height') === 'auto') { + this.closeDropdown(); + collapse_settings(); + setTimeout(() => { + document.getElementById('layout_dropdown').classList.toggle('show'); + }, 10); + } + } + + closeDropdown() { + let dropdowns = document.getElementsByClassName('dropdown-content'); + for (let i = 0; i < dropdowns.length; i++) { + let openDropdown = dropdowns[i]; + if (openDropdown.classList.contains('show')) { + openDropdown.classList.remove('show'); + } + } + } + + setup_download_dropdown() { + //d3.select("#download_dropdown_button").on("mouseenter",showDownloadDropdown); + d3.select('#download_dropdown_button').on('click', () => this.showDownloadDropdown()); + } + + setup_tools_dropdown() { + d3.select('#tools_dropdown_button').on('click', () => this.showToolsDropdown()); + } + + setup_layout_dropdown() { + //d3.select("#layout_dropdown_button").on("mouseover",showLayoutDropdown); + d3.select('#layout_dropdown_button').on('click', () => this.showLayoutDropdown()); + } + + fix() { + if (d3.selectAll('.selected')[0].length === 0) { + d3.selectAll('.node circle').each(d => { + d.fixed = true; + }); + } + d3.selectAll('.selected').each(d => { + d.fixed = true; + }); + } + + unfix() { + d3.selectAll('.selected').each(d => { + d.fixed = false; + }); + if (d3.selectAll('.selected')[0].length === 0) { + d3.selectAll('.node circle').each(d => { + d.fixed = false; + }); + } + } + + redraw() { + if (!this.being_dragged && d3.event.sourceEvent) { + let dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = d3.event.sourceEvent.clientX; + let y = d3.event.sourceEvent.clientY; + x = (x - this.sprites.position.x) / this.sprites.scale.x; + y = (y - this.sprites.position.y) / this.sprites.scale.y; + + let extraX = x * (d3.event.transform.k - this.sprites.scale.x); + let extraY = y * (d3.event.transform.k - this.sprites.scale.y); + this.sprites.position.x += d3.event.sourceEvent.movementX - extraX; + this.sprites.position.y += d3.event.sourceEvent.movementY - extraY; + + this.sprites.scale.x = d3.event.transform.k; + this.sprites.scale.y = d3.event.transform.k; + this.edge_container.position = this.sprites.position; + this.edge_container.scale = this.sprites.scale; + + cloneViewer.clone_edge_container.position = this.sprites.position; + cloneViewer.clone_edge_container.scale = this.sprites.scale; + cloneViewer.clone_sprites.position = this.sprites.position; + cloneViewer.clone_sprites.scale = this.sprites.scale; + + // text_container.position = sprites.position; + // text_container.scale = sprites.scale; + + d3.select('#vis').attr( + 'transform', + 'translate(' + + [this.sprites.position.x, this.sprites.position.y] + + ')' + + ' scale(' + + this.sprites.scale.x + + ')', + ); + } + } + + center_view(on_selected) { + let all_xs = []; + let all_ys = []; + let num_selected = 0; + for (let i = 0; i < this.all_nodes.length; i++) { + if (this.all_outlines[i].selected) { + num_selected += 1; + } + } + for (let i = 0; i < this.all_nodes.length; i++) { + if (!on_selected || this.all_outlines[i].selected || num_selected === 0) { + all_xs.push(this.all_nodes[i].x); + all_ys.push(this.all_nodes[i].y); + } + } + + let minx = d3.min(all_xs); + let maxx = d3.max(all_xs); + let miny = d3.min(all_ys); + let maxy = d3.max(all_ys); + + const dx = maxx - minx + 50; + const dy = maxy - miny + 50; + const x = (maxx + minx) / 2; + const y = (maxy + miny) / 2; + let scale = 0.85 / Math.max(dx / this.width, dy / this.height); + + // perform transition in 750 ms with 25ms steps + let N_STEPS = 5; + let delta_x = (this.width / 2 - ((maxx + minx) / 2) * scale - this.sprites.position.x) / N_STEPS; + let delta_y = (this.height / 2 + 30 - ((maxy + miny) / 2) * scale - this.sprites.position.y) / N_STEPS; + let delta_scale = (scale - this.sprites.scale.x) / N_STEPS; + + let step = 0; + const move = () => { + if (step < N_STEPS) { + this.sprites.position.x += delta_x; + this.sprites.position.y += delta_y; + this.sprites.scale.x += delta_scale; + this.sprites.scale.y += delta_scale; + this.edge_container.position = this.sprites.position; + this.edge_container.scale = this.sprites.scale; + cloneViewer.clone_edge_container.position = this.sprites.position; + cloneViewer.clone_edge_container.scale = this.sprites.scale; + cloneViewer.clone_sprites.position = this.sprites.position; + cloneViewer.clone_sprites.scale = this.sprites.scale; + + d3.select('#vis').attr( + 'transform', + 'translate(' + + [this.sprites.position.x, this.sprites.position.y] + + ')' + + ' scale(' + + this.sprites.scale.x + + ')', + ); + this.zoomer.scaleTo(d3.select('svg').select('g'), this.sprites.scale.x); + step += 1; + setTimeout(move, 10); + } + }; + move(); + } + + save_coords = () => { + if (this.mutable) { + let text = ''; + for (let i in this.coordinates) { + text = text + [i.toString(), this.all_nodes[i].x.toString(), this.all_nodes[i].y.toString()].join(',') + '\n'; + } + let name = window.location.search; + let path = name.slice(1, name.length) + '/coordinates.txt'; + $.ajax({ + data: { path: path, content: text }, + type: 'POST', + url: 'cgi-bin/save_data.py', + }); + } + }; +} diff --git a/scripts_1_6_dev/forceLayout_style.css b/src/forceLayout_style.css similarity index 100% rename from scripts_1_6_dev/forceLayout_style.css rename to src/forceLayout_style.css diff --git a/scripts_1_5_dev/lavish-bootstrap.css b/src/lavish-bootstrap.css similarity index 100% rename from scripts_1_5_dev/lavish-bootstrap.css rename to src/lavish-bootstrap.css diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..9ce47b1 --- /dev/null +++ b/src/main.js @@ -0,0 +1,267 @@ +import * as d3 from 'd3'; + +import CloneViewer from './clone_viewer.js'; +import Cluster from './cluster_script.js'; +import Cluster2 from './cluster2_script.js'; +import ColorBar from './colorBar'; +import DoubletDetector from './doublet_detector.js'; +import DownloadSelectedExpr from './downloadSelectedExpr_script.js'; +import ForceLayout from './forceLayout_script.js'; +import PAGA from './PAGA_viewer.js'; +import SelectionScript from './selection_script.js'; +import SpringPlot from './make_new_SPRINGplot_script.js'; +import SelectionLogic from './selection_logic.js'; +import SmoothingImputation from './smoothing_imputation.js'; +import StickyNote from './stickyNote.js'; + +import { colorpicker_setup } from './colorpicker_layout.js'; +import { getData } from './file_helper'; +import { settings_setup, collapse_settings } from './settings_script.js'; +import { postMessageToParent } from './util.js'; + +/** @type CloneViewer */ +export let cloneViewer; + +/** @type Cluster */ +export let cluster; + +/** @type Cluster2 */ +export let cluster2; + +/** @type ColorBar */ +export let colorBar; + +/** @type DoubletDetector */ +export let doubletDetector; + +/** @type DownloadSelectedExpr */ +export let downloadSelectedExpr; + +/** @type ForceLayout */ +export let forceLayout; + +/** @type PAGA */ +export let paga; + +/** @type SelectionScript */ +export let selectionScript; + +/** @type SelectionLogic */ +export let selectionLogic; + +/** @type SmoothingImputation */ +export let smoothingImputation; + +/** @type SpringPlot */ +export let springPlot; + +/** @type StickyNote */ +export let stickyNote; + +d3.select('#sound_toggle') + .append('img') + .attr('src', 'src/sound_effects/icon_mute.svg') + .on('click', () => { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + d3.select('#sound_toggle') + .select('img') + .attr('src', 'src/sound_effects/icon_mute.svg'); + } else { + d3.select('#sound_toggle') + .select('img') + .attr('src', 'src/sound_effects/icon_speaker.svg'); + } + }); + +let name = window.location.search; +let my_origin = window.location.origin; +let my_pathname = window.location.pathname; +let path_split = my_pathname.split('/'); +let path_start = path_split.slice(0, path_split.length - 1).join('/'); +path_split[path_split.length - 1] = 'springViewer_1_5_dev.html'; + +let dynamic_path = path_split.join('/'); + +d3.select('#changeViewer_link').attr('href', my_origin + dynamic_path + name); +let base_dirs = name.slice(1, name.length).split('/'); +let base_dir_name = base_dirs.slice(0, base_dirs.length - 1).join('/'); + +d3.select('#all_SPRINGplots_menu').attr('href', my_origin + path_start + '/currentDatasetsList.html?' + base_dir_name); + +let tmp = name.slice(1, name.length).split('/'); + +export let graph_directory = tmp.slice(0, tmp.length - 1).join('/'); +export let sub_directory = tmp[tmp.length - 1]; +export let project_directory = graph_directory + '/' + sub_directory; + +document.title = `SPRING Viewer - ${tmp[tmp.length - 1]}`; + +d3.select('#force_layout') + .select('svg') + .remove(); +d3.select('#color_chooser') + .selectAll('div') + .remove(); +d3.select('#color_chooser') + .selectAll('input') + .remove(); +d3.select('#color_chooser') + .selectAll('select') + .remove(); + +const loadData = async () => { + forceLayout = await ForceLayout.create(); + settings_setup(); + colorBar = await getColorBarFromAjax(); + cloneViewer = await CloneViewer.create(); + selectionScript = await SelectionScript.create(); + stickyNote = await StickyNote.create(); + cluster = await Cluster.create(); + cluster2 = await Cluster2.create(); + await setupUserInterface(); +}; + +const getColorBarFromAjax = async args => { + try { + const python_data = await $.ajax({ + data: { base_dir: graph_directory }, + type: 'POST', + url: 'cgi-bin/load_counts.py', + }); + const result = await ColorBar.create(python_data); + return result; + } catch (e) { + const geneData = await getData('genes', 'txt'); + const result = await ColorBar.create(geneData); + return result; + } +}; + +loadData() + .then(res => { + console.log('Spring done loading!'); + postMessageToParent({ type: 'loaded' }); + }) + .catch(e => { + console.log(e); + }); + +const setupUserInterface = async () => { + d3.select('#load_colors').remove(); + forceLayout.initiateButtons(); + forceLayout.setup_download_dropdown(); + forceLayout.setup_tools_dropdown(); + forceLayout.center_view(false); + + cloneViewer.clone_sprites.visible = true; + cloneViewer.edge_container.visible = true; + + forceLayout.animation(); + forceLayout.setup_layout_dropdown(); + + springPlot = SpringPlot.create(); + downloadSelectedExpr = DownloadSelectedExpr.create(); + + smoothingImputation = SmoothingImputation.create(); + doubletDetector = DoubletDetector.create(); + selectionLogic = SelectionLogic.create(); + colorpicker_setup(); + paga = await PAGA.create(); + + // load_text_annotation(); + // stratify_setup(); + // show_stratify_popup(); + // start_clone_viewer(); + // show_imputation_popup(); + // show_colorpicker_popup('HSC_HSC_fate1'); + + window.onclick = function(event) { + if (!event.target.matches('#settings_dropdown *')) { + collapse_settings(); + } + if (!event.target.matches('#download_dropdown_button')) { + forceLayout.closeDropdown(); + } + if (!event.target.matches('#layout_dropdown_button')) { + forceLayout.closeDropdown(); + } + }; + + window.addEventListener('message', event => { + if (!event.isTrusted && event.origin === window.location.origin) { + return; + } + try { + if (typeof event.data === 'string') { + const parsedData = JSON.parse(event.data); + switch (parsedData.type) { + case 'init': { + if (parsedData.payload.categories && parsedData.payload.categories.length >= 1) { + setLabelSelection(parsedData.payload.categories); + window.cacheData.set('categories', parsedData.payload.categories); + } + if (parsedData.payload.indices && parsedData.payload.indices.length >= 1) { + setIndexSelection(parsedData.payload.indices); + window.cacheData.set('indices', parsedData.payload.indices); + } + break; + } + case 'selected-labels-update': { + setLabelSelection(parsedData.payload.selectedLabels); + break; + } + case 'selected-cells-update': { + setIndexSelection(parsedData.payload.indices); + break; + } + default: { + break; + } + } + } + } catch (err) { + console.log(`Unable to parse received message.\n\ + Data: ${event.data} + Error: ${err}`); + } finally { + selectionScript.update_selected_count(); + colorBar.count_clusters(); + } + }); +}; + +const setLabelSelection = labels => { + if (labels) { + const selectedCategory = document.getElementById('labels_menu').value; + const { label_list } = colorBar.getSampleCategoricalColoringData(selectedCategory); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (labels.includes(label_list[i])) { + forceLayout.all_outlines[i].selected = true; + forceLayout.all_outlines[i].tint = '0xffff00'; + forceLayout.all_outlines[i].alpha = forceLayout.all_nodes[i].alpha; + } else { + forceLayout.all_outlines[i].selected = false; + forceLayout.all_outlines[i].alpha = 0; + } + } + } +}; + +const setIndexSelection = indices => { + if (indices) { + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + forceLayout.all_outlines[i].selected = false; + forceLayout.all_outlines[i].alpha = 0; + } + for (const coordinateIndex of indices) { + forceLayout.all_outlines[coordinateIndex].tint = '0xffff00'; + forceLayout.all_outlines[coordinateIndex].selected = true; + forceLayout.all_outlines[coordinateIndex].alpha = forceLayout.all_nodes[coordinateIndex].alpha; + } + } +}; diff --git a/scripts_1_5_dev/make_menu.py b/src/make_menu.py similarity index 100% rename from scripts_1_5_dev/make_menu.py rename to src/make_menu.py diff --git a/src/make_new_SPRINGplot_script.js b/src/make_new_SPRINGplot_script.js new file mode 100755 index 0000000..b7d2fa6 --- /dev/null +++ b/src/make_new_SPRINGplot_script.js @@ -0,0 +1,464 @@ +import * as d3 from 'd3'; +import { graph_directory, sub_directory, forceLayout } from './main'; + +const MAXHEIGHT = 660; +const STARTHEIGHT = 610; + +export default class SpringPlot { + /** @type SpringPlot */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call SpringPlot.create()!'); + } + return this._instance; + } + + static create() { + if (!this._instance) { + this._instance = new SpringPlot(); + return this._instance; + } else { + throw new Error( + 'SpringPlot.create() has already been called, get the existing instance with SpringPlot.instance!', + ); + } + } + + constructor() { + this.custom_genes = ''; + this.include_exclude = 'exclude'; + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'make_new_SPRINGplot_popup'); + + this.button_bar = this.popup + .append('div') + .attr('id', 'make_new_SPRINGplot_button_bar') + .style('width', '100%'); + + this.button_bar.append('text').text('Make new SPRING plot from selection'); + + this.close_button = this.button_bar + .append('button') + .text('Close') + .on('mousedown', () => this.hide_make_new_SPRINGplot_popup()); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Name of plot') + .append('input') + .attr('type', 'text') + .attr('id', 'input_new_dir') + .attr('value', 'E.g. "My_favorite_cells"') + .style('width', '220px'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Email address') + .append('input') + .attr('type', 'text') + .attr('id', 'input_email') + .style('width', '220px'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .attr('id', 'newSPRING_description_box') + .append('label') + .text('Description') + .append('textarea') + .attr('id', 'input_description') + .style('width', '217px') + .style('height', '22px') + .on('keydown', () => { + setTimeout(() => { + let o = d3.select('#input_description'); + o.style('height', '1px'); + o.style('height', o.node().scrollHeight.toString() + 'px'); + if (d3.select('#make_new_SPRINGplot_message_div').style('visibility') === 'hidden') { + this.popup.style( + 'height', + d3.min([$('#newSPRING_description_box').height() + 597.223, STARTHEIGHT]).toString() + 'px', + ); + } else { + this.popup.style( + 'height', + d3.min([$('#newSPRING_description_box').height() + 745, MAXHEIGHT]).toString() + 'px', + ); + } + }, 0); + }); + + this.batch_correction_blurb = this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .attr('id', 'batch_correction_blurb') + .style('width', '320px') + .style('margin-top', '25px'); + + this.batch_correction_blurb + .append('text') + .text('Use cell projection to avoid batch effects. ') + .style('color', 'rgb(220,220,220)'); + this.batch_correction_blurb + .append('text') + .text('Negatively selected ') + .style('color', 'rgb(80,80,255)') + .style('font-weight', '900'); + this.batch_correction_blurb + .append('text') + .text('cells will be projected onto ') + .style('color', 'rgb(220,220,220)'); + this.batch_correction_blurb + .append('text') + .text('positively selected ') + .style('color', 'yellow') + .style('font-weight', '900'); + this.batch_correction_blurb + .append('text') + .text('cells.') + .style('color', 'white'); + + this.optional_params = this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .attr('id', 'make_new_SPRINGplot_optional_params'); + + this.optional_params.append('hr').style('float', 'left'); + this.optional_params.append('text').text('Optional parameters'); + this.optional_params.append('hr').style('float', 'right'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Min expressing cells (gene filtering)') + .append('input') + .attr('type', 'text') + .attr('id', 'input_minCells') + .attr('value', '3'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Min number of UMIs (gene filtering)') + .append('input') + .attr('type', 'text') + .attr('id', 'input_minCounts') + .attr('value', '3'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Gene letiability %ile (gene filtering)') + .append('input') + .attr('type', 'text') + .attr('id', 'input_pctl') + .attr('value', '90'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Number of principal components') + .append('input') + .attr('type', 'text') + .attr('id', 'input_numPC') + .attr('value', '20'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Number of nearest neighbors') + .append('input') + .attr('type', 'text') + .attr('id', 'input_kneigh') + .attr('value', '3'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Number of force layout iterations') + .append('input') + .attr('type', 'text') + .attr('id', 'input_nIter') + .attr('value', '500'); + + this.popup + .append('div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('label') + .text('Save force layout animation') + .append('button') + .text('No') + .attr('id', 'input_animation') + .on('click', () => { + if (d3.select(this).text() === 'Yes') { + d3.select(this).text('No'); + } else { + d3.select(this).text('Yes'); + } + }); + + this.custom_genes_div = this.popup.append('div').attr('class', 'make_new_SPRINGplot_input_div'); + this.custom_genes_div + .append('label') + .append('button') + .text('Exclude') + .attr('id', 'include_exclude_toggle') + .on('click', function() { + if (d3.select(this).text() === 'Exclude') { + d3.select(this).text('Include'); + } else { + d3.select(this).text('Exclude'); + } + }); + + this.wrapper = this.custom_genes_div + .append('label') + .text(' custom gene list') + .append('span') + .attr('id', 'gene_list_upload_wrapper'); + + this.wrapper.append('span').text('Choose file'); + this.wrapper + .append('input') + .attr('type', 'file') + .attr('id', 'gene_list_upload_input') + .on('change', () => { + if (d3.select('#gene_list_upload_input').node().files.length > 0) { + let reader = new FileReader(); + let file = d3.select('#gene_list_upload_input').node().files[0]; + reader.readAsText(file, 'UTF-8'); + reader.onload = evt => { + this.custom_genes = evt.target.result; + this.wrapper.style('padding', '4px 11px 4px 11px'); + this.wrapper.style('background-color', 'red'); + this.wrapper.select('span').text('Uploaded'); + }; + } + }); + + this.popup + .append('div') + .attr('id', 'make_new_SPRINGplot_submission_div') + .attr('class', 'make_new_SPRINGplot_input_div') + .append('button') + .text('Submit') + .on('click', () => this.submit_new_SPRINGplot()); + + this.popup + .append('div') + .attr('id', 'make_new_SPRINGplot_message_div') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .style('overflow', 'scroll') + .append('text'); + + d3.selectAll('.make_new_SPRINGplot_input_div').on('mousedown', () => { + d3.event.stopPropagation(); + }); + + d3.select('#make_new_SPRINGplot_popup').call( + d3 + .drag() + .on('start', () => this.make_new_SPRINGplot_popup_dragstarted()) + .on('drag', () => this.make_new_SPRINGplot_popup_dragged()) + .on('end', () => this.make_new_SPRINGplot_popup_dragended()), + ); + } + // <-- SpringPlot Constructor End --> + + make_new_SPRINGplot_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + make_new_SPRINGplot_popup_dragged() { + let cx = parseFloat( + d3 + .select('#make_new_SPRINGplot_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#make_new_SPRINGplot_popup') + .style('top') + .split('px')[0], + ); + d3.select('#make_new_SPRINGplot_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#make_new_SPRINGplot_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + make_new_SPRINGplot_popup_dragended() { + return; + } + + hide_make_new_SPRINGplot_popup = () => { + d3.select('#make_new_SPRINGplot_popup').style('visibility', 'hidden'); + d3.select('#gene_list_upload_wrapper').style('padding', '4px 8px 4px 8px'); + d3.select('#gene_list_upload_wrapper').style('background-color', 'rgba(0,0,0,.7)'); + d3.select('#gene_list_upload_wrapper') + .select('span') + .text('Choose file'); + }; + + show_make_new_SPRINGplot_popup = () => { + let mywidth = parseInt( + d3 + .select('#make_new_SPRINGplot_popup') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#input_description').style('height', '22px'); + d3.select('#make_new_SPRINGplot_message_div') + .style('visibility', 'hidden') + .style('height', '0px'); + d3.select('#input_description').node().value = ''; + d3.select('#make_new_SPRINGplot_popup') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '10px') + .style('padding-bottom', '0px') + .style('visibility', 'visible') + .style('height', STARTHEIGHT); + }; + + submit_new_SPRINGplot() { + let running_online = false; + this.include_exclude = d3.select('#include_exclude_toggle').text(); + + // Do cgi stuff to check for valid input + // When finished... + let sel2text = ''; + let com2text = ''; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + sel2text = sel2text + ',' + i.toString(); + } + if (forceLayout.all_outlines[i].compared) { + com2text = com2text + ',' + i.toString(); + } + } + sel2text = sel2text.slice(1, sel2text.length); + com2text = com2text.slice(1, com2text.length); + let new_dir = $('#input_new_dir').val(); + let email = $('#input_email').val(); + let description = $('#input_description').val(); + let minCells = $('#input_minCells').val(); + let minCounts = $('#input_minCounts').val(); + let letPctl = $('#input_pctl').val(); + let kneigh = $('#input_kneigh').val(); + let numPC = $('#input_numPC').val(); + let nIter = $('#input_nIter').val(); + let animate = d3.select('#input_animation').text(); + let this_url = window.location.href; + + let output_message = ''; + let subplot_script = ''; + + if (running_online) { + output_message = 'Checking input...'; + subplot_script = 'cgi-bin/spring_from_selection2.online.py'; + } else { + output_message = 'Please wait...
'; + output_message += + "If everything goes smoothly, a link to your new subplot will appear when ready, and you'll receive a link via email (if provided).
"; + output_message += '
This may take several minutes.
'; + subplot_script = 'cgi-bin/spring_from_selection2.py'; + } + + //let MAXHEIGHT = 772; + + d3.select('#make_new_SPRINGplot_popup') + .transition() + .duration(200) + .style('height', d3.min([$('#newSPRING_description_box').height() + 745, MAXHEIGHT]).toString() + 'px'); + + d3.select('#make_new_SPRINGplot_message_div') + .transition() + .duration(200) + .style('height', '120px') + .each(() => { + d3.select('#make_new_SPRINGplot_message_div').style('visibility', 'inherit'); + d3.select('#make_new_SPRINGplot_message_div') + .select('text') + .html(output_message); + }); + + console.log(subplot_script); + $.ajax({ + data: { + animate: animate, + base_dir: graph_directory, + compared_cells: com2text, + current_dir: sub_directory, + custom_genes: this.custom_genes, + description: description, + email: email, + include_exclude: this.include_exclude, + kneigh: kneigh, + minCells: minCells, + minCounts: minCounts, + nIter: nIter, + new_dir: new_dir, + numPC: numPC, + selected_cells: sel2text, + this_url: this_url, + letPctl: letPctl, + }, + success: success_message => { + console.log(success_message); + + if (running_online) { + let orig_message = success_message; + d3.select('#make_new_SPRINGplot_message_div') + .select('text') + .html(orig_message); + + function checkLog() { + jQuery.get(graph_directory + '/' + new_dir + '/lognewspring2.txt', logdata => { + let logdata_split = logdata.split('\n'); + let display_message = orig_message + '
' + logdata_split[logdata_split.length - 2]; + d3.select('#make_new_SPRINGplot_message_div') + .select('text') + .html(display_message); + if (!display_message.endsWith('
')) { + setTimeout(checkLog, 500); + } + }); + } + + if (orig_message.endsWith('several minutes.
\n') || orig_message.endsWith('email.
\n')) { + setTimeout(checkLog, 500); + } + } else { + d3.select('#make_new_SPRINGplot_message_div') + .select('text') + .html(success_message); + } + }, + type: 'POST', + url: subplot_script, + }); + } +} diff --git a/scripts_1_6_dev/make_new_SPRINGplot_style.css b/src/make_new_SPRINGplot_style.css similarity index 100% rename from scripts_1_6_dev/make_new_SPRINGplot_style.css rename to src/make_new_SPRINGplot_style.css diff --git a/src/newSpringPlot.js b/src/newSpringPlot.js new file mode 100755 index 0000000..0302730 --- /dev/null +++ b/src/newSpringPlot.js @@ -0,0 +1,22 @@ +import { forceLayout, graph_directory, sub_directory } from './main'; + +function newSpringPlot(callback) { + console.log('boop'); + + let sel2text = ''; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + sel2text = sel2text + ',' + i.toString(); + } + } + sel2text = sel2text.slice(1, sel2text.length); + $.ajax({ + data: { base_dir: graph_directory, current_dir: sub_directory, new_dir: 'poop', selected_cells: sel2text }, + success: data => { + console.log(data); + $('#updater').html(data); + }, + type: 'POST', + url: 'cgi-bin/spring_from_selection2.py', + }); +} diff --git a/src/rotation_script.js b/src/rotation_script.js new file mode 100755 index 0000000..f5d4495 --- /dev/null +++ b/src/rotation_script.js @@ -0,0 +1,189 @@ +import * as d3 from 'd3'; + +import { forceLayout, selectionScript } from './main'; + +export const rotation_update = () => { + let selected = []; + let stash_i = forceLayout.stashed_coordinates.length; + forceLayout.stashed_coordinates.push({}); + for (let i in forceLayout.all_nodes) { + if (forceLayout.all_outlines[i].selected) { + selected.push(i); + } + forceLayout.stashed_coordinates[stash_i][i] = [forceLayout.all_nodes[i].x, forceLayout.all_nodes[i].y]; + } + if (selected.length === 0) { + selectionScript.deselect_all(); + selected = d3.range(0, forceLayout.all_nodes.length); + } + let real_scale = 1; + + const vis = d3.select('#vis'); + vis.attr( + 'transform', + 'translate(' + [forceLayout.sprites.x, forceLayout.sprites.y] + ')' + ' scale(' + forceLayout.sprites.scale.x + ')', + ); + vis.append('circle').attr('id', 'rotation_outer_circ'); + vis.append('circle').attr('id', 'rotation_inner_circ'); + vis.append('circle').attr('id', 'rotation_pivot'); + + rotation_show(); + d3.select('#rotation_pivot').style('opacity', 1); + + let all_xs = []; + let all_ys = []; + for (let i in forceLayout.all_nodes) { + if (forceLayout.all_outlines[i].selected) { + all_xs.push(forceLayout.all_nodes[i].x); + all_ys.push(forceLayout.all_nodes[i].y); + } + } + let cx = d3.sum(all_xs) / all_xs.length; + let cy = d3.sum(all_ys) / all_ys.length; + let dels = []; + for (let i = 0; i < all_xs.length; i++) { + dels.push(Math.sqrt(Math.pow(all_xs[i] - cx, 2) + Math.pow(all_ys[i] - cy, 2))); + } + let rotator_radius = d3.median(dels) * 1.5; + + const zoomScale = d3.zoomTransform(vis.node()).k; + + d3.select('#rotation_pivot') + .attr('r', d3.min([13 / zoomScale, (rotator_radius + 30) / 3])) + .style('stroke-width', d3.min([3 / zoomScale, 10])) + .style('cx', cx) + .style('cy', cy); + d3.select('#rotation_outer_circ') + .attr('r', rotator_radius + 30 + 12 / zoomScale) + .style('cx', cx) + .style('cy', cy) + .style('stroke-width', 18 / zoomScale); + d3.select('#rotation_inner_circ') + .attr('r', rotator_radius + 30) + .style('cx', cx) + .style('cy', cy) + .style('stroke-width', 6 / zoomScale); + + d3.select('#rotation_outer_circ') + .on('mouseover', () => { + d3.select('#rotation_outer_circ').style('opacity', 0.5); + }) + .on('mouseout', () => { + d3.select('#rotation_outer_circ').style('opacity', 0); + }); + + d3.select('#rotation_pivot').call( + d3 + .drag() + .on('start', pivot_dragstarted) + .on('drag', pivot_dragged) + .on('end', pivot_dragended), + ); + + d3.select('#rotation_outer_circ').call( + d3 + .drag() + .on('start', handle_dragstarted) + .on('drag', handle_dragged) + .on('end', handle_dragended), + ); + + function pivot_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + function pivot_dragged() { + let cxFromD3 = parseFloat( + d3 + .select('#rotation_pivot') + .style('cx') + .split('px')[0], + ); + let cyFromD3 = parseFloat( + d3 + .select('#rotation_pivot') + .style('cy') + .split('px')[0], + ); + d3.select('#rotation_pivot').style('cx', cxFromD3 + d3.event.dx); + d3.select('#rotation_pivot').style('cy', cyFromD3 + d3.event.dy); + d3.select('#rotation_inner_circ').style('cx', cxFromD3 + d3.event.dx); + d3.select('#rotation_inner_circ').style('cy', cyFromD3 + d3.event.dy); + d3.select('#rotation_outer_circ').style('cx', cxFromD3 + d3.event.dx); + d3.select('#rotation_outer_circ').style('cy', cyFromD3 + d3.event.dy); + } + + function pivot_dragended() { + return; + } + + function handle_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + d3.select('#rotation_outer_circ').style('opacity', 0.5); + real_scale = 1; + } + + function handle_dragged() { + let cxFromD3 = parseFloat( + d3 + .select('#rotation_pivot') + .style('cx') + .split('px')[0], + ); + let cyFromD3 = parseFloat( + d3 + .select('#rotation_pivot') + .style('cy') + .split('px')[0], + ); + + let r1 = Math.atan((d3.event.y - cyFromD3) / (d3.event.x - cxFromD3)); + let r2 = Math.atan((d3.event.y + d3.event.dy - cyFromD3) / (d3.event.x + d3.event.dx - cxFromD3)); + let rot = r1 - r2; + if (r1 - r2 > 1.4) { + rot += 3.141592653; + } + + let d1 = Math.sqrt((d3.event.y - cyFromD3) ** 2 + (d3.event.x - cxFromD3) ** 2); + let d2 = Math.sqrt((d3.event.y + d3.event.dy - cyFromD3) ** 2 + (d3.event.x + d3.event.dx - cxFromD3) ** 2); + real_scale = (real_scale * d2) / d1; + let scale = 0; + if (Math.abs(real_scale - 1) > 0.5) { + scale = d2 / d1; + } else { + scale = 1; + } + + if (Math.abs(rot) < 1) { + for (let i in forceLayout.all_outlines) { + if (forceLayout.all_outlines[i].selected) { + let d = forceLayout.all_nodes[i]; + let dx = d.x - cxFromD3; + let dy = d.y - cyFromD3; + let brad = Math.sqrt(dx * dx + dy * dy); + let ddx = Math.cos(rot) * dx + Math.sin(rot) * dy; + let ddy = -Math.sin(rot) * dx + Math.cos(rot) * dy; + let arad = Math.sqrt(ddx * ddx + ddy * ddy); + forceLayout.move_node(i, cxFromD3 + ddx * scale, cyFromD3 + ddy * scale); + } + } + forceLayout.adjust_edges(); + } + } + + function handle_dragended() { + d3.select('#rotation_outer_circ').style('opacity', 0); + } +}; + +export const rotation_show = () => { + d3.select('#rotation_outer_circ').style('visibility', 'visible'); + d3.select('#rotation_inner_circ').style('visibility', 'visible'); + d3.select('#rotation_pivot').style('visibility', 'visible'); +}; + +export const rotation_hide = () => { + d3.select('#rotation_outer_circ').style('visibility', 'hidden'); + d3.select('#rotation_inner_circ').style('visibility', 'hidden'); + d3.select('#rotation_pivot').style('visibility', 'hidden'); +}; diff --git a/scripts_1_6_dev/selection_logic.css b/src/selection_logic.css similarity index 100% rename from scripts_1_6_dev/selection_logic.css rename to src/selection_logic.css diff --git a/src/selection_logic.js b/src/selection_logic.js new file mode 100644 index 0000000..34716f4 --- /dev/null +++ b/src/selection_logic.js @@ -0,0 +1,233 @@ +import * as d3 from 'd3'; + +import { colorBar, forceLayout, selectionScript } from './main'; + +export default class SelectionLogic { + /** @type SelectionLogic */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call SelectionLogic.create()!'); + } + return this._instance; + } + + static create() { + if (!this._instance) { + this._instance = new SelectionLogic(); + return this._instance; + } else { + throw new Error( + 'SelectionLogic.create() has already been called, get the existing instance with SelectionLogic.instance!', + ); + } + } + + constructor() { + this.selection_data = {}; + + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'selection_logic_popup'); + + this.add_new_bar = this.popup + .append('div') + .attr('id', 'selection_logic_add_new_bar') + .on('mousedown', () => { + d3.event.stopPropagation(); + }); + + this.add_new_bar + .append('label') + .text('Selection name ') + .append('input') + .attr('id', 'selection_logic_input'); + this.add_new_bar + .append('button') + .text('Close') + .on('click', this.hide_selection_logic_popup); + this.add_new_bar + .append('button') + .text('Clear') + .on('click', () => this.clear_options()); + this.add_new_bar + .append('button') + .text('Add') + .on('click', () => this.add_selection()); + + this.andor_bar = this.popup.append('div').attr('id', 'selection_logic_andor_bar'); + this.left_dropdown = this.andor_bar.append('select').style('margin-right', '20px'); + this.andor_bar + .append('button') + .text('AND') + .style('margin-right', '12px') + .on('click', () => this.apply_and); + this.andor_bar + .append('button') + .text('OR') + .on('click', () => this.apply_or); + + this.right_dropdown = this.andor_bar.append('select').style('margin-left', '20px'); + + this.left_dropdown.append('option').text('Current selection'); + this.right_dropdown.append('option').text('Current selection'); + + d3.select('#selection_logic_popup').call( + d3 + .drag() + .on('start', () => this.selection_logic_popup_dragstarted()) + .on('drag', () => this.selection_logic_popup_dragged()) + .on('end', () => this.selection_logic_popup_dragended()), + ); + } + // <-- SelectionLogic Constructor End --> + + get_selections() { + let left_name = this.left_dropdown.property('value'); + let right_name = this.right_dropdown.property('value'); + let left_sel = []; + if (left_name === 'Current selection') { + left_sel = this.get_selected_cells(); + } else { + left_sel = this.selection_data[left_name]; + } + let right_sel = []; + if (right_name === 'Current selection') { + right_sel = this.get_selected_cells(); + } else { + right_sel = this.selection_data[right_name]; + } + return [left_sel, right_sel]; + } + + union_arrays(x, y) { + let obj = {}; + for (let i = x.length - 1; i >= 0; --i) { + obj[x[i]] = x[i]; + } + for (let i = y.length - 1; i >= 0; --i) { + obj[y[i]] = y[i]; + } + let res = []; + for (let k in obj) { + if (obj.hasOwnProperty(k)) { + // <-- optional + res.push(obj[k]); + } + } + return res; + } + + apply_or() { + let sels = this.get_selections(); + let new_sel = this.union_arrays(sels[0], sels[1]); + this.set_selections(new_sel); + this.get_selections(); + } + + apply_and() { + let sels = this.get_selections(); + let new_sel = sels[0].filter(n => { + return sels[1].indexOf(n) !== -1; + }); + this.set_selections(new_sel); + } + + set_selections(sel) { + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + forceLayout.all_outlines[i].selected = false; + forceLayout.all_outlines[i].alpha = 0; + } + for (let i = 0; i < sel.length; i++) { + forceLayout.all_outlines[sel[i]].tint = '0xffff00'; + forceLayout.all_outlines[sel[i]].selected = true; + forceLayout.all_outlines[sel[i]].alpha = forceLayout.all_nodes[sel[i]].alpha; + } + selectionScript.update_selected_count(); + colorBar.count_clusters(); + } + + clear_options() { + this.left_dropdown.selectAll('option').remove(); + this.left_dropdown.append('option').text('Current selection'); + this.right_dropdown.selectAll('option').remove(); + this.right_dropdown.append('option').text('Current selection'); + this.selection_data = {}; + } + + add_selection() { + let name = $('#selection_logic_input').val(); + $('#selection_logic_input').val(''); + this.left_dropdown + .append('option') + .text(name.toString()) + .attr('selected', 'selected'); + this.right_dropdown + .append('option') + .text(name.toString()) + .attr('selected', 'selected'); + this.selection_data[name.toString()] = this.get_selected_cells(); + } + + get_selected_cells() { + let sel = []; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + sel.push(i); + } + } + return sel; + } + + selection_logic_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + selection_logic_popup_dragged() { + let cx = parseFloat( + d3 + .select('#selection_logic_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#selection_logic_popup') + .style('top') + .split('px')[0], + ); + d3.select('#selection_logic_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#selection_logic_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + selection_logic_popup_dragended() { + return; + } + + show_selection_logic_popup() { + let mywidth = parseInt( + d3 + .select('#selection_logic_popup') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#selection_logic_popup') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '220px') + .style('visibility', 'visible'); + } + + hide_selection_logic_popup() { + d3.select('#selection_logic_popup').style('visibility', 'hidden'); + } +} diff --git a/src/selection_script.js b/src/selection_script.js new file mode 100755 index 0000000..79ba491 --- /dev/null +++ b/src/selection_script.js @@ -0,0 +1,591 @@ +import * as d3 from 'd3'; +import { rotation_hide } from './rotation_script'; +import { colorBar, forceLayout } from './main'; +import { postMessageToParent, postSelectedCellUpdate } from './util'; + +export default class SelectionScript { + /** @type SelectionScript */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call SelectionScript.create()!'); + } + return this._instance; + } + + static create() { + if (!this._instance) { + this._instance = new SelectionScript(); + return this._instance; + } else { + throw new Error( + 'SelectionScript.create() has already been called, get the existing instance with SelectionScript.instance!', + ); + } + } + + get svg_width() { + const result = parseInt(d3.select('svg').attr('width'), 10); + return result ? result : 0; + } + + constructor() { + this.selection_mode = 'drag_pan_zoom'; + this.drag_pan_zoom_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 177) + .attr('y', 0) + .attr('fill-opacity', 0.5) + .attr('width', 200) + .attr('height', 24) + .on('click', () => { + this.selection_mode = 'drag_pan_zoom'; + this.switch_mode(); + }); + + this.positive_select_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 177) + .attr('y', 24) + .attr('fill-opacity', 0.15) + .attr('width', 200) + .attr('height', 24) + .on('click', () => { + this.selection_mode = 'positive_select'; + this.switch_mode(); + }); + + this.negative_select_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 177) + .attr('y', 48) + .attr('fill-opacity', 0.15) + .attr('width', 200) + .attr('height', 24) + .on('click', () => { + this.selection_mode = 'negative_select'; + this.switch_mode(); + }); + + this.deselect_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 177) + .attr('y', 72) + .attr('fill-opacity', 0.15) + .attr('width', 200) + .attr('height', 24) + .on('click', () => { + this.selection_mode = 'deselect'; + this.switch_mode(); + }); + + this.pos_select_count_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'selection_option') + .attr('x', this.svg_width) + .attr('y', 103) + .attr('fill-opacity', 0.25) + .attr('width', 200) + .attr('height', 24); + + this.neg_select_count_rect = d3 + .select('svg') + .append('rect') + .attr('class', 'selection_option') + .attr('x', this.svg_width) + .attr('y', 127) + .attr('fill-opacity', 0.25) + .attr('width', 200) + .attr('height', 24); + + this.switch_div = d3 + .select('#force_layout') + .append('div') + .attr('id', 'selection_switch_div') + .style('position', 'absolute') + .style('top', '35px') + .style('right', '0px') + .style('width', '20px') + .style('height', '30px') + .on('click', () => this.switch_pos_neg()) + .append('img') + .attr('src', 'stuff/switch_arrow.png') + .style('height', '100%') + .style('width', '8px') + .style('margin-left', '8px'); + + d3.select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 167) + .attr('y', 16) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'white') + .text('Drag/pan/zoom'); + + d3.select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 167) + .attr('y', 40) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'yellow') + .text('Positive select (shift)'); + + d3.select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 167) + .attr('y', 64) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'blue') + .text('Negative select (Shift+Esc)'); + + d3.select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'selection_option') + .attr('x', this.svg_width - 167) + .attr('y', 88) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'white') + .text('Deselect (command)'); + this.pos_select_count_text = d3 + .select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'selection_option') + .attr('x', this.svg_width) + .attr('y', 119) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'yellow') + .text('0 cells selected'); + + this.neg_select_count_text = d3 + .select('svg') + .append('text') + .attr('pointer-events', 'none') + .attr('class', 'selection_option') + .attr('x', this.svg_width) + .attr('y', 143) + .attr('font-family', 'sans-serif') + .attr('font-size', '12px') + .attr('fill', 'blue') + .text('0 cells selected'); + + d3.select('body') + .on('keydown', () => this.keydown()) + .on('keyup', () => this.keyup()); + + this.base_radius = parseInt(d3.select('#settings_range_node_size').attr('value'), 10) / 100; + this.large_radius = this.base_radius * 3; + + this.setup_brusher(); + + // "(De)select All" button + d3.select('#deselect') + .select('button') + .on('click', () => this.deselect_all()); + } + // <-- SelectionScript Constructor End --> + + setup_brusher() { + this.brusher = d3 + .brush() + .on('brush', () => { + let extent = d3 + .selectAll('.brush .selection') + .node() + .getBoundingClientRect(); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + let d = forceLayout.all_nodes[i]; + let dim = document.getElementById('svg_graph').getBoundingClientRect(); + let x = d.x * forceLayout.sprites.scale.x + dim.left; + let y = d.y * forceLayout.sprites.scale.y + dim.top; + x = x + forceLayout.sprites.position.x; + y = y + forceLayout.sprites.position.y; + + let inrect = extent.left <= x && x < extent.right && extent.top <= y && y < extent.bottom; + let o = forceLayout.all_outlines[i]; + if (this.selection_mode === 'positive_select' || this.selection_mode === 'negative_select') { + o.selected = (o.selected && !o.compared) || (this.selection_mode === 'positive_select' && inrect); + o.compared = (o.compared && !o.selected) || (this.selection_mode === 'negative_select' && inrect); + } + if (this.selection_mode === 'deselect') { + o.selected = o.selected && !inrect; + o.compared = o.compared && !inrect; + } + if (o.selected) { + o.alpha = forceLayout.all_nodes[i].alpha; + o.tint = '0xffff00'; + } + if (o.compared) { + o.alpha = forceLayout.all_nodes[i].alpha; + o.tint = '0x0000ff'; + } + if (o.selected || o.compared) { + // all_outlines[i].scale.set(large_radius); + // all_nodes[i].scale.set(large_radius); + } else { + o.alpha = 0; + // all_outlines[i].scale.set(base_radius); + // all_nodes[i].scale.set(base_radius); + } + } + this.update_selected_count(); + colorBar.count_clusters(); + }) + .on('end', d => { + // Ensures we don't recursively call 'brush end' events: https://github.com/d3/d3-brush/issues/25 + if (d3.event.sourceEvent.type !== 'end') { + this.brusher.move(this.brush, null); + } + + let selected = []; + let indices = []; + for (let i = 0; i < forceLayout.all_outlines.length; ++i) { + if (forceLayout.all_outlines[i].selected) { + selected.push(forceLayout.all_outlines[i]); + indices.push(i); + } + } + if (selected.length === 0) { + rotation_hide(); + } + postSelectedCellUpdate(indices); + }); + + this.brush = d3 + .select('#svg_graph') + .append('g') + .datum(() => { + return { selected: false }; + }) + .attr('class', 'brush'); + + this.brush + .call(this.brusher) + .on('mousedown.brush', null) + .on('touchstart.brush', null) + .on('touchmove.brush', null) + .on('touchend.brush', null); + + this.brush.select('.background').style('cursor', 'auto'); + } + + switch_pos_neg() { + let pos_cells = []; + let neg_cells = []; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + pos_cells.push(i); + } + if (forceLayout.all_outlines[i].compared) { + neg_cells.push(i); + } + } + for (let i = 0; i < neg_cells.length; i++) { + forceLayout.all_outlines[neg_cells[i]].tint = '0xffff00'; + forceLayout.all_outlines[neg_cells[i]].selected = true; + forceLayout.all_outlines[neg_cells[i]].compared = false; + } + for (let i = 0; i < pos_cells.length; i++) { + forceLayout.all_outlines[pos_cells[i]].tint = '0x0000ff'; + forceLayout.all_outlines[pos_cells[i]].compared = true; + forceLayout.all_outlines[pos_cells[i]].selected = false; + } + } + + switch_mode() { + this.drag_pan_zoom_rect.transition('5').attr('fill-opacity', this.selection_mode === 'drag_pan_zoom' ? 0.5 : 0.15); + this.positive_select_rect + .transition() + .duration(5) + .attr('fill-opacity', this.selection_mode === 'positive_select' ? 0.5 : 0.15); + this.negative_select_rect + .transition() + .duration(5) + .attr('fill-opacity', this.selection_mode === 'negative_select' ? 0.5 : 0.15); + this.deselect_rect.transition('5').attr('fill-opacity', this.selection_mode === 'deselect' ? 0.5 : 0.15); + if (this.selection_mode !== 'drag_pan_zoom') { + d3.select('#svg_graph').select('g') + .call(forceLayout.zoomer) + .on('mousedown.zoom', null) + .on('touchstart.zoom', null) + .on('touchmove.zoom', null) + .on('touchend.zoom', null); + + this.brush.select('.background').style('cursor', 'crosshair'); + this.brush.call(this.brusher); + } + if (this.selection_mode === 'drag_pan_zoom') { + this.brush + .call(this.brusher) + .on('mousedown.brush', null) + .on('touchstart.brush', null) + .on('touchmove.brush', null) + .on('touchend.brush', null); + this.brush.select('.background').style('cursor', 'auto'); + d3.select('#svg_graph').select('g').call(forceLayout.zoomer); + } + } + + keydown() { + let shiftKey = d3.event.shiftKey; + let metaKey = d3.event.metaKey; // command key on a mac + let keyCode = d3.event.keyCode; + + if (shiftKey && keyCode !== 27) { + this.selection_mode = 'positive_select'; + } + if (shiftKey && keyCode === 27) { + this.selection_mode = 'negative_select'; + } + if (metaKey) { + this.selection_mode = 'deselect'; + } + this.switch_mode(); + } + + keyup() { + let shiftKey = d3.event.shiftKey || d3.event.metaKey; + let ctrlKey = d3.event.ctrlKey; + let keyCode = 0; + this.selection_mode = 'drag_pan_zoom'; + this.switch_mode(); + } + + retract_edge_toggle(callback) { + d3.select('#edge_toggle_image') + .transition() + .duration(400) + .attr('x', this.svg_width); + d3.select('#show_edges_rect') + .transition() + .duration(400) + .attr('x', this.svg_width); + d3.select('#edge_text') + .selectAll('tspan') + .transition() + .duration(400) + .attr('x', this.svg_width) + .each(() => callback()); + } + + update_selected_count() { + let num_selected = 0; + let num_compared = 0; + const indices = new Array(); + + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (forceLayout.all_outlines[i].selected) { + num_selected += 1; + indices.push(i); + } + if (forceLayout.all_outlines[i].compared) { + num_compared += 1; + } + } + + if (num_selected === 0 && this.pos_count_extended) { + this.pos_count_extended = false; + if (!this.neg_count_extended) { + this.pos_select_count_rect.transition('500').attr('x', this.svg_width); + this.pos_select_count_text + .transition('500') + .attr('x', this.svg_width) + .each(() => this.extend_edge_toggle()); + } else { + this.pos_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width); + this.pos_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width); + } + } + if (num_selected !== 0) { + if (!this.pos_count_extended) { + this.pos_count_extended = true; + if (!this.neg_count_extended) { + this.retract_edge_toggle(() => { + this.pos_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width - 177); + this.pos_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width - 167); + }); + } else { + this.pos_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width - 177); + this.pos_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width - 167); + } + } + let pct = Math.floor((num_selected / forceLayout.all_nodes.length) * 100); + this.pos_select_count_text.text(num_selected.toString() + ' cells selected (' + pct.toString() + '%)'); + } + if (num_compared === 0 && this.neg_count_extended) { + this.neg_count_extended = false; + if (!this.pos_count_extended) { + this.neg_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width); + this.neg_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width) + .each(() => this.extend_edge_toggle()); + } else { + this.neg_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width); + this.neg_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width); + } + } + if (num_compared !== 0) { + if (!this.neg_count_extended) { + this.neg_count_extended = true; + if (!this.pos_count_extended) { + this.retract_edge_toggle(() => { + this.neg_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width - 177); + this.neg_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width - 167); + }); + } else { + this.neg_select_count_rect + .transition() + .duration(500) + .attr('x', this.svg_width - 177); + this.neg_select_count_text + .transition() + .duration(500) + .attr('x', this.svg_width - 167); + } + } + let newPct = Math.floor((num_compared / forceLayout.all_nodes.length) * 100); + this.neg_select_count_text.text(num_compared.toString() + ' cells selected (' + newPct.toString() + '%)'); + } + } + + extend_edge_toggle() { + d3.select('#edge_toggle_image') + .transition() + .duration(400) + .attr('x', this.svg_width - 77); + d3.select('#show_edges_rect') + .transition() + .duration(400) + .attr('x', this.svg_width - 177); + d3.select('#edge_text') + .selectAll('tspan') + .transition() + .duration(400) + .attr('x', this.svg_width - 167); + } + + deselect_all() { + let any_selected = false; + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (forceLayout.all_outlines[i].selected) { + any_selected = true; + } + if (forceLayout.all_outlines[i].compared) { + any_selected = true; + } + } + if (any_selected) { + rotation_hide(); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + forceLayout.all_outlines[i].alpha = 0; + forceLayout.all_outlines[i].selected = false; + forceLayout.all_outlines[i].compared = false; + + // all_nodes[i].scale.set(base_radius); + // all_outlines[i].scale.set(base_radius); + } + } + if (!any_selected) { + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + forceLayout.all_outlines[i].alpha = forceLayout.all_nodes[i].alpha; + forceLayout.all_outlines[i].tint = '0xffff00'; + forceLayout.all_outlines[i].selected = true; + // all_outlines[i].scale.set(large_radius); + // all_nodes[i].scale.set(large_radius); + } + } + + postSelectedCellUpdate([]) + colorBar.count_clusters(); + this.update_selected_count(); + } + + loadSelectedCells(project_directory) { + // load selected cells if it exists + let selection_filename = project_directory + '/selected_cells.txt'; + let new_selection = new Array(); + d3.text(selection_filename).then(text => { + text.split('\n').forEach((entry, index, array) => { + if (entry !== '') { + new_selection.push(parseInt(entry, 10)); + } + }); + d3.selectAll('.node circle').classed('selected', d => { + if (new_selection.indexOf(d.name) >= 0) { + d.selected = true; + return true; + } + }); + }); + } + + extend_selection() { + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (forceLayout.all_outlines[i].selected) { + for (let j in forceLayout.neighbors[i]) { + let jj = forceLayout.neighbors[i][j]; + forceLayout.all_outlines[jj].alpha = forceLayout.all_nodes[i].alpha; + forceLayout.all_outlines[jj].tint = '0xffff00'; + forceLayout.all_outlines[jj].selected = true; + } + } + } + } +} diff --git a/scripts_1_5_dev/selection_style.css b/src/selection_style.css similarity index 100% rename from scripts_1_5_dev/selection_style.css rename to src/selection_style.css diff --git a/src/settings_script.js b/src/settings_script.js new file mode 100755 index 0000000..aa5b79f --- /dev/null +++ b/src/settings_script.js @@ -0,0 +1,293 @@ +import * as d3 from 'd3'; + +import { forceLayout } from './main'; +import { SPRITE_IMG_WIDTH, rgbToHex } from './util'; + +export const settings_setup = () => { + console.log('setting up settings_setup'); + let dropdown = d3 + .select('#button_panel') + .append('div') + .attr('id', 'settings_dropdown'); + + let slider_scroller = dropdown.append('div').attr('id', 'settings_slider_scroller'); + + dropdown + .append('div') + .attr('class', 'settings_slider_box') + .style('height', '40px') + .style('background-color', 'rgb(100,100,100)') + .append('button') + .attr('id', 'settings_restore_defaults_button') + .style('visibility', 'hidden') + .text('Restore defaults') + .on('click', restore_defaults); + + //add_slider_box(slider_scroller,"settings_range_node_repulsion","Physics: node repulsion") + //add_slider_box(slider_scroller,"settings_range_link_distance","Physics: link distance") + //add_slider_box(slider_scroller,"settings_range_link_strength","Physics: link strength") + //add_slider_box(slider_scroller,"settings_range_gravity","Physics: gravity") + add_slider_box(slider_scroller, 'settings_range_node_size', 'Layout: node size'); + add_slider_box(slider_scroller, 'settings_range_node_opacity', 'Layout: node opacity'); + add_slider_box(slider_scroller, 'settings_range_background_color', 'Layout: background color'); + //add_slider_box(slider_scroller,"settings_range_edge_color","Layout: edge color") + //add_slider_box(slider_scroller,"settings_range_edge_thickness","Layout: edge thickness") + add_slider_box(slider_scroller, 'settings_range_edge_opacity', 'Layout: edge opacity'); + + //d3.select("#settings").on("mouseenter", expand_settings); + d3.select('#settings').on('click', toggle_settings); + + d3.select('#settings_range_node_repulsion') + .attr('min', 0) + .attr('max', 300) + .attr('value', 50) + .on('input', function() { + node_repulsion_change(this.value); + }); + + d3.select('#settings_range_link_distance') + .attr('min', 0) + .attr('max', 500) + .attr('value', 40) + .on('input', function() { + link_distance_change(this.value); + }); + + d3.select('#settings_range_link_strength') + .attr('min', 0) + .attr('max', 100) + .attr('value', 20) + .on('input', function() { + link_strength_change(this.value); + }); + + d3.select('#settings_range_gravity') + .attr('min', 0) + .attr('max', 40) + .attr('value', 5) + .on('input', function() { + gravity_change(this.value); + }); + + d3.select('#settings_range_node_size') + .attr('min', 0) + .attr('max', 200) + .attr('value', 50) + .on('input', function() { + node_size_change(this.value); + }); + + d3.select('#settings_range_node_opacity') + .attr('min', 0) + .attr('max', 100) + .attr('value', 80) + .on('input', function() { + node_opacity_change(this.value); + }); + + d3.select('#settings_range_background_color') + .attr('min', 0) + .attr('max', 255) + .attr('value', 220) + .on('input', function() { + background_color_change(this.value); + }); + + d3.select('#settings_range_edge_color') + .attr('min', 0) + .attr('max', 255) + .attr('value', 100) + .on('input', function() { + edge_color_change(this.value); + }); + + d3.selectAll('.link line').attr('stroke-width', 1.2); + d3.select('#settings_range_edge_thickness') + .attr('min', 0) + .attr('max', 800) + .attr('value', 120) + .on('input', function() { + edge_thickness_change(this.value); + }); + + d3.select('#settings_range_edge_opacity') + .attr('min', 0) + .attr('max', 100) + .attr('value', 50) + .on('input', function() { + edge_opacity_change(this.value); + }); + + d3.select('#quick_dark') + .select('button') + .on('click', function() { + d3.select('#settings_range_background_color').node().value = '20'; + background_color_change('20'); + }); + + d3.select('#quick_light') + .select('button') + .on('click', function() { + d3.select('#settings_range_background_color').node().value = '220'; + background_color_change('220'); + }); + + function node_repulsion_change(val) { + forceLayout.force.force('repulsion', d3.forceManyBody().strength(-val / 10)); + forceLayout.force.restart(); + } + + function link_distance_change(val) { + forceLayout.force.force('linkDistance', d3.forceLink().strength(val / 10)); + forceLayout.force.restart(); + } + + function link_strength_change(val) { + forceLayout.force.force('linkDistance', d3.forceLink().strength(val / 10)); + forceLayout.force.restart(); + } + + function gravity_change(val) { + forceLayout.force.force('gravity', d3.forceY(val)); + forceLayout.force.tick(); + } + + function check_any_selected() { + let any_selected = false; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + any_selected = true; + } + } + return any_selected; + } + + function node_size_change(val) { + val = (val / 50) ** 2.5 * 50 * (32 / SPRITE_IMG_WIDTH); + let any_selected = check_any_selected(); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (!any_selected || forceLayout.all_outlines[i].selected) { + forceLayout.all_nodes[i].scale.set(val / 100); + forceLayout.all_outlines[i].scale.set(val / 100); + } + } + } + + function node_opacity_change(val) { + let any_selected = check_any_selected(); + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + if (!any_selected || forceLayout.all_outlines[i].selected) { + forceLayout.all_nodes[i].alpha = val / 100; + if (forceLayout.all_outlines[i].selected || forceLayout.all_outlines[i].compared) { + forceLayout.all_outlines[i].alpha = val / 100; + } + } + } + } + + function background_color_change(val) { + const parsedVal = parseInt(val, 10); + forceLayout.app.renderer.backgroundColor = parseInt(rgbToHex(parsedVal, parsedVal, parsedVal), 16); + /* + if (val < 125) { + text_anos.ba.style.fill = '#B8B8B8'; + } else { + text_anos.ba.style.fill = '#303030'; + } + */ + } + + function edge_color_change(val) { + return; + } + function edge_thickness_change(val) { + d3.selectAll('.link line').attr('stroke-width', val / 100); + } + + function edge_opacity_change(val) { + forceLayout.edge_container.alpha = val / 100; + } + + function rgb_string(val) { + return 'rgb(' + val.toString() + ',' + val.toString() + ',' + val.toString() + ')'; + } + + function add_slider_box(host, id, name) { + let slider_box = host.append('div').attr('class', 'settings_slider_box'); + slider_box + .append('input') + .attr('type', 'range') + .attr('id', id); + slider_box.append('p').text(name); + } + + function restore_defaults() { + /* + document.getElementById("settings_range_node_repulsion").value = 50; + node_repulsion_change(50); + + document.getElementById("settings_range_link_distance").value = 40; + link_distance_change(40); + + document.getElementById("settings_range_link_strength").value = 20; + link_strength_change(20); + + document.getElementById("settings_range_gravity").value = 5; + gravity_change(5); + */ + + document.getElementById('settings_range_node_size').value = 50; + node_size_change(50); + + document.getElementById('settings_range_node_opacity').value = 80; + node_opacity_change(80); + + document.getElementById('settings_range_background_color').value = 220; + background_color_change(220); + + //document.getElementById("settings_range_edge_color").value = 100; + //edge_color_change(100); + + //document.getElementById("settings_range_edge_thickness").value = 120; + //edge_thickness_change(120); + + document.getElementById('settings_range_edge_opacity').value = 50; + edge_opacity_change(50); + } +}; + +export const toggle_settings = () => { + if (d3.select('#settings_dropdown').style('visibility') === 'hidden') { + expand_settings(); + } else { + collapse_settings(); + } +}; + +export const expand_settings = () => { + forceLayout.closeDropdown(); + if (d3.select('#settings_dropdown').style('visibility') === 'hidden') { + setTimeout(function() { + d3.select('#settings_dropdown').style('height', '275px'); + d3.select('#settings_dropdown').style('visibility', 'visible'); + d3.select('#settings_restore_defaults_button').style('visibility', 'visible'); + }, 5); + } +}; + +export const collapse_settings = () => { + if ( + parseInt( + d3 + .select('#settings_dropdown') + .style('height') + .split('px')[0], + 10, + ) > 200 + ) { + forceLayout.closeDropdown(); + d3.select('#settings_dropdown').style('height', '0px'); + d3.select('#settings_restore_defaults_button').style('visibility', 'hidden'); + d3.select('#settings_dropdown').style('visibility', 'hidden'); + } +}; diff --git a/scripts_1_6_dev/settings_style.css b/src/settings_style.css similarity index 100% rename from scripts_1_6_dev/settings_style.css rename to src/settings_style.css diff --git a/scripts_1_6_dev/smoothing_imputation.css b/src/smoothing_imputation.css similarity index 100% rename from scripts_1_6_dev/smoothing_imputation.css rename to src/smoothing_imputation.css diff --git a/src/smoothing_imputation.js b/src/smoothing_imputation.js new file mode 100644 index 0000000..2563a16 --- /dev/null +++ b/src/smoothing_imputation.js @@ -0,0 +1,287 @@ +import * as d3 from 'd3'; +import * as Spinner from 'spinner'; + +import { forceLayout, colorBar, cloneViewer, graph_directory, sub_directory } from './main'; + +export default class SmoothingImputation { + /** @type SmoothingImputation */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call SmoothingImputation.create()!'); + } + return this._instance; + } + + static create() { + if (!this._instance) { + this._instance = new SmoothingImputation(); + return this._instance; + } else { + throw new Error( + 'SmoothingImputation.create() has already been called, get the existing instance with SmoothingImputation.instance!', + ); + } + } + + constructor() { + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'imputation_popup'); + + this.button_bar = this.popup + .append('div') + .attr('id', 'imputation_button_bar') + .on('mousedown', () => { + d3.event.stopPropagation(); + }); + + this.button_bar + .append('label') + .text('N = ') + .append('input') + .attr('id', 'imputation_N_input') + .property('value', 10); + this.button_bar + .append('label') + .text('\u03B2 = ') + .append('input') + .attr('id', 'imputation_beta_input') + .property('value', 0.1); + this.button_bar + .append('button') + .text('Restore') + .on('click', () => this.restore_colors()); + this.button_bar + .append('button') + .text('Smooth') + .on('click', () => this.perform_smoothing()); + this.button_bar + .append('button') + .text('Close') + .on('click', () => this.hide_imputation_popup()); + + this.text_box = this.popup + .append('div') + .attr('id', 'imputation_description') + .append('text') + .text('Smooth gene expression on the graph. Increase N or decrease \u03B2 to enhance the degree of smoothing.'); + + d3.select('#imputation_popup').call( + d3 + .drag() + .on('start', () => this.imputation_popup_dragstarted()) + .on('drag', () => this.imputation_popup_dragged()) + .on('end', () => this.imputation_popup_dragended()), + ); + } + // <-- SmoothingImputation Constructor End --> + + imputation_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + + imputation_popup_dragged() { + let cx = parseFloat( + d3 + .select('#imputation_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#imputation_popup') + .style('top') + .split('px')[0], + ); + d3.select('#imputation_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#imputation_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + imputation_popup_dragended() { + return; + } + + show_waiting_wheel() { + this.popup.append('div').attr('id', 'wheel_mask'); + let opts = { + className: 'spinner', // The CSS class to assign to the spinner + color: '#000', // #rgb or #rrggbb or array of colors + corners: 1, // Corner roundness (0..1) + direction: 1, // 1: clockwise, -1: counterclockwise + fps: 20, // Frames per second when using setTimeout() as a fallback for CSS + hwaccel: true, // Whether to use hardware acceleration + left: '50%', // Left position relative to parent + length: 35, // The length of each line + lines: 17, // The number of lines to draw + opacity: 0.2, // Opacity of the lines + position: 'relative', // Element positioning + radius: 50, // The radius of the inner circle + rotate: 8, // The rotation offset + scale: 0.22, // Scales overall size of the spinner + shadow: false, // Whether to render a shadow + speed: 0.9, // Rounds per second + top: '50%', // Top position relative to parent + trail: 60, // Afterglow percentage + width: 15, // The line thickness + zIndex: 2e9, // The z-index (defaults to 2000000000) + }; + let target = document.getElementById('wheel_mask'); + let spinner = new Spinner(opts).spin(target); + $(target).data('spinner', spinner); + } + + restore_colors() { + colorBar.setNodeColors(); + } + + hide_waiting_wheel() { + $('.spinner').remove(); + $('#wheel_mask').remove(); + } + + perform_smoothing() { + if (true) { + let t0 = new Date(); + //update_slider(); + let beta = $('#imputation_beta_input').val(); + let N = $('#imputation_N_input').val(); + + let all_r = ''; + let all_g = ''; + let all_b = ''; + let sel = ''; + let sel_nodes = []; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + let col = {}; + if (forceLayout.all_nodes[i].tint === '0x000000' && cloneViewer.clone_nodes[i] === undefined) { + col = { r: 0, b: 0, g: 0 }; + } else { + col = forceLayout.base_colors[i]; + } + if (forceLayout.all_outlines[i].selected) { + sel = sel + ',' + i.toString(); + sel_nodes.push(i); + } + all_r = all_r + ',' + col.r.toString(); + all_g = all_g + ',' + col.g.toString(); + all_b = all_b + ',' + col.b.toString(); + } + all_r = all_r.slice(1, all_r.length); + all_g = all_g.slice(1, all_g.length); + all_b = all_b.slice(1, all_b.length); + sel = '#' + sel.slice(1, sel.length); + + this.show_waiting_wheel(); + console.log(sel); + $.ajax({ + data: { + base_dir: graph_directory, + beta: beta, + n_rounds: N, + raw_b: all_b, + raw_g: all_g, + raw_r: all_r, + selected: sel, + sub_dir: graph_directory + '/' + sub_directory, + }, + //data: {base_dir:graph_directory, sub_dir:graph_directory+'/'+sub_directory, beta:beta, n_rounds:N, raw_g:green_string}, + success: data => { + if (data && data.length >= 1) { + let t1 = new Date(); + console.log('Smoothed the data: ', t1.getTime() - t0.getTime()); + let datasplit = data.split('|'); + let new_min = parseFloat(datasplit[0]) - 0.02; + let new_max = parseFloat(datasplit[1]); + + let current_min = 0; + let current_max = 0; + + if (document.getElementById('channels_button').checked) { + current_min = 0; + current_max = parseFloat(d3.max(colorBar.green_array)); + } else { + current_max = parseFloat(d3.max(forceLayout.base_colors.map(colorBar.max_color))); + current_min = parseFloat(d3.min(forceLayout.base_colors.map(colorBar.min_color))); + } + + function nrm(x) { + return ( + ((parseFloat(x) - new_min + current_min) / (new_max - new_min + 0.01)) * (current_max - current_min) + ); + } + + let spl = datasplit[2].split(';'); + let reds = spl[0].split(',').map(nrm); + let greens = spl[1].split(',').map(nrm); + let blues = spl[2].split(',').map(nrm); + + if (document.getElementById('channels_button').checked) { + colorBar.green_array = greens; + greens = greens.map(x => colorBar.normalize_one_val(x)); + } + + if (sel_nodes.length === 0) { + for (let i = 0; i < forceLayout.all_nodes.length; i++) { + forceLayout.base_colors[i] = { + r: Math.floor(reds[i]), + g: Math.floor(greens[i]), + b: Math.floor(blues[i]), + }; + } + } else { + for (let i = 0; i < sel_nodes.length; i++) { + forceLayout.base_colors[sel_nodes[i]] = { + r: Math.floor(reds[i]), + g: Math.floor(greens[i]), + b: Math.floor(blues[i]), + }; + } + } + + colorBar.updateColorMax(); + + forceLayout.app.stage.children.sort((a, b) => { + return ( + colorBar.average_color(forceLayout.base_colors[a.tabIndex]) - + colorBar.average_color(forceLayout.base_colors[b.tabIndex]) + ); + }); + } else { + console.log('Got empty smoothing data.'); + } + this.hide_waiting_wheel(); + }, + type: 'POST', + url: 'cgi-bin/smooth_gene.py', + }); + } + } + + show_imputation_popup() { + let mywidth = parseInt( + d3 + .select('#imputation_popup') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#imputation_popup') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '80px') + .style('visibility', 'visible'); + } + + hide_imputation_popup() { + d3.select('#imputation_popup').style('visibility', 'hidden'); + } +} diff --git a/scripts_1_5_dev/sound_effects/download_sound.wav b/src/sound_effects/download_sound.wav similarity index 100% rename from scripts_1_5_dev/sound_effects/download_sound.wav rename to src/sound_effects/download_sound.wav diff --git a/scripts_1_5_dev/sound_effects/download_sound_data/e00/d00/e0000813.au b/src/sound_effects/download_sound_data/e00/d00/e0000813.au similarity index 100% rename from scripts_1_5_dev/sound_effects/download_sound_data/e00/d00/e0000813.au rename to src/sound_effects/download_sound_data/e00/d00/e0000813.au diff --git a/scripts_1_5_dev/sound_effects/download_sound_data/e00/d00/e0000e79.au b/src/sound_effects/download_sound_data/e00/d00/e0000e79.au similarity index 100% rename from scripts_1_5_dev/sound_effects/download_sound_data/e00/d00/e0000e79.au rename to src/sound_effects/download_sound_data/e00/d00/e0000e79.au diff --git a/scripts_1_5_dev/sound_effects/icon_mute.svg b/src/sound_effects/icon_mute.svg similarity index 100% rename from scripts_1_5_dev/sound_effects/icon_mute.svg rename to src/sound_effects/icon_mute.svg diff --git a/scripts_1_5_dev/sound_effects/icon_speaker.svg b/src/sound_effects/icon_speaker.svg similarity index 100% rename from scripts_1_5_dev/sound_effects/icon_speaker.svg rename to src/sound_effects/icon_speaker.svg diff --git a/scripts_1_5_dev/sound_effects/openclose_sound.wav b/src/sound_effects/openclose_sound.wav similarity index 100% rename from scripts_1_5_dev/sound_effects/openclose_sound.wav rename to src/sound_effects/openclose_sound.wav diff --git a/scripts_1_5_dev/sound_effects/opennew_sound.wav b/src/sound_effects/opennew_sound.wav similarity index 100% rename from scripts_1_5_dev/sound_effects/opennew_sound.wav rename to src/sound_effects/opennew_sound.wav diff --git a/scripts_1_5_dev/spring_loader_style.css b/src/spring_loader_style.css similarity index 100% rename from scripts_1_5_dev/spring_loader_style.css rename to src/spring_loader_style.css diff --git a/scripts_1_6_dev/stickyNote.css b/src/stickyNote.css similarity index 100% rename from scripts_1_6_dev/stickyNote.css rename to src/stickyNote.css diff --git a/src/stickyNote.js b/src/stickyNote.js new file mode 100644 index 0000000..42c269b --- /dev/null +++ b/src/stickyNote.js @@ -0,0 +1,350 @@ +import * as d3 from 'd3'; +import sweetAlert from 'sweetalert'; + +import { forceLayout, colorBar, selectionScript, project_directory } from './main'; + +export default class StickyNote { + /** @type StickyNote */ + static _instance; + + static get instance() { + if (!this._instance) { + throw new Error('You must first call StickyNote.create()!'); + } + return this._instance; + } + + static async create() { + if (!this._instance) { + this._instance = new StickyNote(); + await this._instance.loadData(); + return this._instance; + } else { + throw new Error( + 'StickyNote.create() has already been called, get the existing instance with StickyNote.instance!', + ); + } + } + + constructor() { + this.popup = d3 + .select('#force_layout') + .append('div') + .attr('id', 'stickyNote_popup') + .on('mousedown', this.deactivate_all); + + this.button_bar = this.popup + .append('div') + .attr('id', 'stickyNote_button_bar') + .style('width', '100%'); + + this.selected_note = null; + + this.button_bar + .append('button') + .text('Close') + .style('margin-right', '11px') + .on('click', () => { + if (!this.is_synched()) { + this.hide_stickyNote_popup(); + } else { + this.hide_stickyNote_popup(); + } + }); + + this.button_bar + .append('button') + .text('Save') + .on('click', this.save_note) + .attr('id', 'sticky_save_button'); + + this.button_bar + .append('button') + .text('New') + .on('click', this.new_note); + + this.button_bar + .append('button') + .text('Delete') + .on('click', this.delete_note); + + this.button_bar + .append('button') + .text('Bind cells') + .on('click', this.bind_cells); + + this.button_bar + .append('button') + .text('Show selected') + .on('click', this.show_selected); + + this.sticky_div = this.popup.append('div').attr('id', 'sticky_div'); + + this.button_bar.selectAll('button').on('mousedown', () => { + d3.event.stopPropagation(); + }); + + this.popup + .append('div') + .attr('id', 'sticky_email') + .append('label') + .text('Email address') + .append('input') + .attr('type', 'text') + .on('mousedown', () => { + d3.event.stopPropagation(); + }) + .attr('id', 'sticky_email_input') + .style('width', '252px'); + + d3.select('#stickyNote_popup').call( + d3 + .drag() + .on('start', this.stickyNote_popup_dragstarted) + .on('drag', this.stickyNote_popup_dragged) + .on('end', this.stickyNote_popup_dragended), + ); + + return this; + } + // <-- StickyNote Constructor End --> + + async loadData() { + this.sticky_path = project_directory + '/sticky_notes_data.json'; + try { + await $.get(this.sticky_path); + const data = await d3.json(this.sticky_path); + data.forEach(d => { + this.new_note(d); + }); + } catch (e) { + const note = this.new_note(); + this.activate_note(note); + } + } + delete_note() { + d3.selectAll('.sticky_note').each((d, i) => { + if (d3.select(d).attr('active') === 'true') { + d3.select(d).remove(); + } + }); + } + + new_note(d) { + if (!d) { + d = { text: '', emails: '', bound_cells: this.get_selected_cells().join(',') }; + } + + let note = this.sticky_div.insert('div', ':first-child').attr('class', 'sticky_note'); + note.append('textarea').style('height', '90px'); + note.on('mousedown', () => { + d3.event.stopPropagation(); + if (note.attr('active') !== 'true') { + this.deactivate_all(); + this.activate_note(note); + } + }); + note.attr('bound_cells', d.bound_cells); + note.attr('saved_text', d.text); + note.attr('emails', d.emails); + note.select('textarea').text(d.text); + note.select('p').text(d.text); + + return note; + } + + activate_note(note) { + if (note.attr('active') !== 'true') { + note.select('p').style('visibility', 'hidden'); + $(note.select('textarea')).focus(); + + // let emails = note.attr('emails'); + // if (emails.length > 0) { + // $('#sticky_email_input').value = emails.split(',')[1]; + // } + } + + note.style('border', 'solid 2px rgba(230,230,230,.8)').attr('active', 'true'); + + let my_nodes = []; + note + .attr('bound_cells') + .split(',') + .forEach(d => { + if (d !== '') { + my_nodes.push(parseInt(d, 10)); + forceLayout.all_outlines[d].selected = true; + forceLayout.all_outlines[d].tint = '0xffff00'; + forceLayout.all_outlines[d].alpha = 1; + } + }); + colorBar.count_clusters(); + selectionScript.update_selected_count(); + colorBar.shrinkNodes(10, 10, my_nodes, forceLayout.all_nodes); + } + + deactivate_all() { + d3.selectAll('.sticky_note') + .attr('active', 'false') + .style('border', '0px'); + + d3.selectAll('.sticky_note') + .selectAll('textarea') + .style('background-color', 'transparent') + .style('border', 'none'); + } + + sync_note(note) { + if (note.attr('saved_text') !== $(note.select('textarea').node()).val()) { + note.attr('saved_text', $(note.select('textarea').node()).val()); + let my_emails = note.attr('emails').split(','); + let current_email = $('#sticky_email_input').val(); + if (my_emails.indexOf(current_email) === -1) { + my_emails.push(current_email); + note.attr('emails', my_emails.join(',')); + } + } + } + + bind_cells(note) { + let sel = this.get_selected_cells().join(','); + d3.selectAll('.sticky_note').each(d => { + if (d3.select(d).attr('active') === 'true') { + d3.select(d).attr('bound_cells', sel); + } + }); + } + + get_selected_cells() { + let sel = []; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + sel.push(i); + } + } + return sel; + } + + save_note() { + if (!this.check_email()) { + this.flash_email(); + return false; + } else { + d3.selectAll('.sticky_note').each(d => { + this.sync_note(d3.select(d)); + }); + this.write_data(); + return true; + } + } + + write_data() { + let all_data = []; + d3.selectAll('.sticky_note').each(d => { + let note = d3.select(d); + let text = note.attr('saved_text'); + let emails = note.attr('emails'); + let bound_cells = note.attr('bound_cells'); + if (text !== '') { + let my_data = { text: text, emails: emails, bound_cells: bound_cells }; + all_data.push(my_data); + } + }); + const path = project_directory + '/sticky_notes_data.json'; + $.ajax({ + data: { path: path, content: JSON.stringify(all_data, null, ' ') }, + success: () => { + sweetAlert({ title: 'All stickies have been saved' }); + }, + type: 'POST', + url: 'cgi-bin/save_sticky.py', + }); + } + + check_email() { + const emailInput = $('#sticky_email_input').val(); + if (typeof emailInput === 'string' && emailInput.indexOf('@') > -1) { + return true; + } else { + return false; + } + } + + flash_email() { + $('#sticky_email_input').addClass('flash'); + setTimeout(() => { + $('#sticky_email_input').removeClass('flash'); + }, 500); + } + + is_synched() { + return true; + } + + stickyNote_popup_dragstarted() { + d3.event.sourceEvent.stopPropagation(); + } + stickyNote_popup_dragged() { + let cx = parseFloat( + d3 + .select('#stickyNote_popup') + .style('left') + .split('px')[0], + ); + let cy = parseFloat( + d3 + .select('#stickyNote_popup') + .style('top') + .split('px')[0], + ); + d3.select('#stickyNote_popup').style('left', (cx + d3.event.dx).toString() + 'px'); + d3.select('#stickyNote_popup').style('top', (cy + d3.event.dy).toString() + 'px'); + } + + stickyNote_popup_dragended() { + return; + } + + show_selected() { + let selected_cells = []; + for (let i = 0; i < forceLayout.all_outlines.length; i++) { + if (forceLayout.all_outlines[i].selected) { + selected_cells.push(i.toString()); + } + } + d3.selectAll('.sticky_note').style('background-color', 'rgba(0,0,0,.5)'); + d3.selectAll('.sticky_note').each(d => { + let note = d3.select(d); + const bound_cells = note.attr('bound_cells').split(','); + if (bound_cells.filter(n => selected_cells.indexOf(n) >= 0).length > 0) { + note.style('background-color', 'rgba(255,255,0,.4)'); + $(this.sticky_div.node()).prepend(note.node()); + } + }); + } + + hide_stickyNote_popup() { + d3.select('#stickyNote_popup').style('visibility', 'hidden'); + } + + show_stickyNote_popup() { + let mywidth = parseInt( + d3 + .select('#stickyNote_popup') + .style('width') + .split('px')[0], + 10, + ); + let svg_width = parseInt( + d3 + .select('svg') + .style('width') + .split('px')[0], + 10, + ); + d3.select('#stickyNote_popup') + .style('left', (svg_width / 2 - mywidth / 2).toString() + 'px') + .style('top', '10px') + .style('visibility', 'visible'); + } +} diff --git a/src/sticky_page_script.js b/src/sticky_page_script.js new file mode 100644 index 0000000..96b2270 --- /dev/null +++ b/src/sticky_page_script.js @@ -0,0 +1,68 @@ +import * as d3 from 'd3'; +import { openInNewTab } from './util'; + +function add_sticky_subdir(project_directory, sub_directory, order) { + d3.json(project_directory + '/' + sub_directory + '/sticky_notes_data.json').then(data => { + let list_item = d3 + .select('#dataset_list') + .append('li') + .style('order', order); + let header = list_item.append('div').attr('class', 'list_item_header'); + header.append('h3').text(sub_directory); + header.append('p').text(' - ' + data.length.toString() + ' sticky notes'); + + let all_stickies = list_item.append('div').attr('class', 'all_stickies'); + data.forEach(d => { + let sticky = all_stickies.append('div').attr('class', 'one_sticky'); + sticky.append('p').text(d.text); + let emails = d.emails; + if (emails[0] === ',') { + emails = emails.slice(1, emails.length); + } + sticky.append('p').text(emails.split(',').join(', ')); + }); + + list_item + .append('text') + .attr('class', 'show_more_less_text') + .text('Expand') + .on('click', () => { + d3.event.stopPropagation(); + if (d3.select(this).text() === 'Expand') { + list_item.transition('200').style('height', $(list_item.node())[0].scrollHeight.toString() + 'px'); + d3.select(this).text('Collapse'); + } else { + list_item.transition('200').style('height', '46px'); + d3.select(this).text('Expand'); + } + }); + + list_item.on('click', () => { + let my_origin = window.location.origin; + let my_pathname_split = window.location.pathname.split('/'); + let my_pathname_new = my_pathname_split.slice(0, my_pathname_split.length - 1).join('/') + '/springViewer.html'; + let my_url_new = my_origin + my_pathname_new + '?' + project_directory + '/' + sub_directory; + console.log(my_url_new); + openInNewTab(my_url_new); + }); + }); +} + +function populate_sticky_subdirs_list(project_directory) { + let title = project_directory.split('/'); + title = title[title.length - 1]; + d3.select('#project_directory_title').text('Sticky notes from "' + title + '"'); + + $.ajax({ + data: { path: project_directory, filename: 'sticky_notes_data.json' }, + success: output_message => { + let subdirs = output_message.split(','); + console.log(subdirs); + for (let i in subdirs) { + add_sticky_subdir(project_directory, subdirs[i], i + 1); + } + }, + type: 'POST', + url: 'cgi-bin/list_directories_with_filename.py', + }); +} diff --git a/scripts_1_6_dev/sticky_page_style.css b/src/sticky_page_style.css similarity index 100% rename from scripts_1_6_dev/sticky_page_style.css rename to src/sticky_page_style.css diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..457fd09 --- /dev/null +++ b/src/util.js @@ -0,0 +1,86 @@ +import * as d3 from 'd3'; + +export const SPRITE_IMG_WIDTH = 32; + +export const read_csv = text => { + const dict = {}; + text.split('\n').forEach((entry, index, array) => { + if (entry.length > 0) { + let items = entry.split(','); + let gene = items[0]; + const exp_array = []; + items.forEach((e, i, a) => { + if (i > 0) { + exp_array.push(parseFloat(e)); + } + }); + dict[gene] = exp_array; + } + }); + return dict; +}; + +export const openInNewTab = url => { + let win = window.open(url, '_blank'); + win.focus(); +}; + +export const UrlExists = url => { + $.get(url) + .done(() => { + console.log('yes'); + }) + .fail(() => { + console.log('no'); + }); +}; + +export const makeTextFile = text => { + let textFile = ''; + let data = new Blob([text], { type: 'text/plain' }); + + // If we are replacing a previously generated file we need to + // manually revoke the object URL to avoid memory leaks. + window.URL.revokeObjectURL(textFile); + + textFile = window.URL.createObjectURL(data); + return textFile; +}; + +export const downloadFile = (text, name) => { + if ( + d3 + .select('#sound_toggle') + .select('img') + .attr('src') === 'src/sound_effects/icon_speaker.svg' + ) { + let snd = new Audio('src/sound_effects/download_sound.wav'); + snd.play(); + } + let hiddenElement = document.createElement('a'); + hiddenElement.href = 'data:attachment/text,' + encodeURI(text); + hiddenElement.target = '_blank'; + hiddenElement.download = name; + document.body.appendChild(hiddenElement); + hiddenElement.click(); +}; + +export const componentToHex = c => { + let hex = c.toString(16); + return hex.length === 1 ? '0' + hex : hex; +}; + +export const rgbToHex = (r, g, b) => { + return '0x' + componentToHex(r) + componentToHex(g) + componentToHex(b); +}; + +export const postMessageToParent = message => { + if (document.referrer.length > 0 && window.location.origin !== document.referrer) { + window.parent.postMessage(message, document.referrer); + } +}; + +export const postSelectedCellUpdate = indices => { + const currentCategory = document.getElementById('labels_menu').value; + postMessageToParent({ type: 'selected-cells-update', payload: { currentCategory, indices } }); +}; diff --git a/start_server.sh b/start_server.sh index 4d24617..52f7578 100755 --- a/start_server.sh +++ b/start_server.sh @@ -1,2 +1,7 @@ #!/bin/bash -python -m CGIHTTPServer 8000 + +# python 2: +#python -m CGIHTTPServer 8000 + +# python 3: +python -m http.server --bind localhost --cgi 8000 diff --git a/stickyPage.html b/stickyPage.html index 1fd80e9..0c208a1 100644 --- a/stickyPage.html +++ b/stickyPage.html @@ -2,8 +2,8 @@ - - + + @@ -17,9 +17,9 @@

- - - + + +