diff --git a/bin/profile_single_projectr_tsne_run.py b/bin/profile_single_projectr_tsne_run.py
index cb5552f3..a0841ade 100644
--- a/bin/profile_single_projectr_tsne_run.py
+++ b/bin/profile_single_projectr_tsne_run.py
@@ -835,8 +835,8 @@ def run_tsne(dataset_id):
# Rename to end the confusion
adata.var = adata.var.rename(columns={adata.var.columns[0]: "ensembl_id"})
# Modify the AnnData object to not include any duplicated gene symbols (keep only first entry)
+ scanpy_copy = ana.dataset_path().replace('.h5ad', '.scanpy_dups_removed.h5ad')
if len(df.columns) > 1:
- scanpy_copy = ana.dataset_path().replace('.h5ad', '.scanpy_dups_removed.h5ad')
if os.path.exists(scanpy_copy):
os.remove(scanpy_copy)
adata = adata[:, adata.var.index.duplicated() == False].copy(filename=scanpy_copy)
@@ -845,7 +845,7 @@ def run_tsne(dataset_id):
try:
basis = PLOT_TYPE_TO_BASIS[plot_type]
except:
- raise("{} was not a valid plot type".format(plot_type))
+ raise Exception("{} was not a valid plot type".format(plot_type))
# NOTE: This may change in the future if users want plots by group w/o the colorize_by plot added
if plot_by_group:
@@ -993,6 +993,9 @@ def run_tsne(dataset_id):
plt.clf()
plt.close() # Prevent zombie plots, which can cause issues
+ if os.path.exists(scanpy_copy):
+ os.remove(scanpy_copy)
+
return {
"success": success,
"message": message,
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 72b9654b..304ce665 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -57,8 +57,7 @@ RUN apt-get -qq update \
# Required for R
gfortran \
# Required for rpy2
- r-base-dev \
- r-base \
+ r-cran-rjava \
# Required for R-package devtools (which is required for SJD)
libharfbuzz-dev \
libfribidi-dev \
diff --git a/docker/install_bioc.R b/docker/install_bioc.R
index e7a25f04..a671613c 100755
--- a/docker/install_bioc.R
+++ b/docker/install_bioc.R
@@ -1,5 +1,6 @@
#!/usr/bin/env Rscript --vanilla
install.packages(c("BiocManager", "devtools"), dependencies=TRUE, repos="http://lib.stat.cmu.edu/R/CRAN/")
+BiocManager::install(version = "3.19") # required for R 4.4.0
BiocManager::install(c("genesofeve/projectR", "biomaRt"), ask=FALSE)
library(devtools); install_github("CHuanSite/SJD")
\ No newline at end of file
diff --git a/docker/install_bioc.sh b/docker/install_bioc.sh
index 000b4a9d..06df8dec 100755
--- a/docker/install_bioc.sh
+++ b/docker/install_bioc.sh
@@ -5,7 +5,7 @@ Rver="${Rmaj}.4.0"
current_dir=$(pwd)
-curl -s -L http://lib.stat.cmu.edu/R/CRAN/src/base/${Rmaj}/${Rver}.tar.gz | tar xzv -C /opt
+curl -s -L http://lib.stat.cmu.edu/R/CRAN/src/base/${Rmaj}/${Rver}.tar.gz | tar xzv -C /opt || exit 1
cd /opt/${Rver}
/opt/${Rver}/configure --with-readline=no --enable-R-shlib --enable-BLAS-shlib --with-x=no || exit 1
make || exit 1
diff --git a/docker/requirements.txt b/docker/requirements.txt
index 7f6d6700..fcb088bd 100644
--- a/docker/requirements.txt
+++ b/docker/requirements.txt
@@ -20,7 +20,7 @@ more_itertools==9.0.0
mysql-connector-python==8.0.20
numba==0.58.1
numexpr==2.8.4
-numpy==1.26.0
+numpy==1.26.4
opencv-python==4.5.5.64
openpyxl==3.1.5
pandas==2.2.1
@@ -29,11 +29,12 @@ pika==1.3.1
plotly==5.6.0
python-dotenv==0.20.0
requests==2.31.0
-rpy2==3.5.1 # 3.5.2 and up gives errors with rpy2py and py2rpy
+rpy2==3.5.16
sanic
scanpy==1.10.1
scikit-learn==1.0.2
scipy==1.11.04
seaborn==0.13.2
+shadows==0.1a0
tables==3.9.2 # Read hdf5 files into pandas
xlrd==1.2.0
diff --git a/docs/setup.python.md b/docs/setup.python.md
index 16c8a262..6b3bfa0c 100644
--- a/docs/setup.python.md
+++ b/docs/setup.python.md
@@ -60,7 +60,7 @@ Check the requirement.txt file in /docker for the latest packages
mysql-connector-python==8.0.20 \
numba==0.58.1 \
numexpr==2.8.4 \
- numpy==1.26.0 \
+ numpy==1.26.4 \
opencv-python==4.5.5.64 \
openpyxl==3.1.5 \
pandas==2.2.1 \
@@ -75,6 +75,7 @@ Check the requirement.txt file in /docker for the latest packages
scikit-learn==1.0.2 \
scipy==1.11.04 \
seaborn==0.13.2 \
+ shadows==0.1a0 \
tables==3.9.2 \
xlrd==1.2.0
$ sudo mkdir /opt/bin
diff --git a/lib/gear/plotting.py b/lib/gear/plotting.py
index 5d9dfa99..66dc2936 100644
--- a/lib/gear/plotting.py
+++ b/lib/gear/plotting.py
@@ -335,7 +335,7 @@ def _update_by_plot_type(fig, plot_type, force_overlay=False, use_jitter=False):
def generate_plot(df, x=None, y=None, z=None, facet_row=None, facet_col=None,
color_name=None, colormap=None, palette=None,
- reverse_palette=False, category_orders=None,
+ reverse_palette=False, category_orders={},
plot_type='scatter', hide_x_labels=False, hide_y_labels=False,
hide_legend=None, text_name=None, jitter=False,
x_range=None, y_range=None, vlines=[], x_title=None, y_title=None,
@@ -374,7 +374,7 @@ def generate_plot(df, x=None, y=None, z=None, facet_row=None, facet_col=None,
, "facet_row":facet_row
, "facet_col":facet_col
, "color":color_name
- , "category_orders": category_orders if category_orders else {}
+ , "category_orders": category_orders
, "labels":labels_dict
, "hover_name": text_name if text_name else "y_rounded"
}
diff --git a/lib/geardb.py b/lib/geardb.py
index bc273a50..0b59f5d9 100644
--- a/lib/geardb.py
+++ b/lib/geardb.py
@@ -613,7 +613,10 @@ def __init__(self, id=None, dataset_id=None, user_id=None, session_id=None, labe
def __repr__(self):
pipeline_file = self.settings_path()
- return open(pipeline_file).read()
+ json_data = json.loads(open(pipeline_file).read())
+ # change "user_session_id" to "session_id" for consistency
+ json_data['session_id'] = json_data.pop('user_session_id')
+ return json.dumps(json_data, indent=4)
def _serialize_json(self):
# Called when json modules attempts to serialize
@@ -1979,10 +1982,13 @@ def get_shape(self, session_id=None, tuple_only=False):
## File is under datasets/${id}.h5ad
h5ad_file_path = self.get_file_path(session_id=session_id)
- import scanpy as sc
- sc.settings.verbosity = 0
- adata = sc.read_h5ad(h5ad_file_path)
+ from shadows import AnnDataShadow
+ adata = AnnDataShadow(h5ad_file_path)
+
(n_obs, n_vars) = adata.shape
+
+ adata.close()
+
if tuple_only:
return (n_obs, n_vars)
diff --git a/services/projectr/Dockerfile b/services/projectr/Dockerfile
index ed642cae..92adabcc 100644
--- a/services/projectr/Dockerfile
+++ b/services/projectr/Dockerfile
@@ -11,8 +11,7 @@ RUN apt-get -qq update \
# Required for R
gfortran \
# Required for rpy2
- r-base-dev \
- r-base \
+ r-cran-rjava \
# Required for R-package devtools (which is required for SJD)
libharfbuzz-dev \
libfribidi-dev \
diff --git a/services/projectr/install_bioc.R b/services/projectr/install_bioc.R
index e7a25f04..a671613c 100755
--- a/services/projectr/install_bioc.R
+++ b/services/projectr/install_bioc.R
@@ -1,5 +1,6 @@
#!/usr/bin/env Rscript --vanilla
install.packages(c("BiocManager", "devtools"), dependencies=TRUE, repos="http://lib.stat.cmu.edu/R/CRAN/")
+BiocManager::install(version = "3.19") # required for R 4.4.0
BiocManager::install(c("genesofeve/projectR", "biomaRt"), ask=FALSE)
library(devtools); install_github("CHuanSite/SJD")
\ No newline at end of file
diff --git a/services/projectr/install_bioc.sh b/services/projectr/install_bioc.sh
index 000b4a9d..06df8dec 100755
--- a/services/projectr/install_bioc.sh
+++ b/services/projectr/install_bioc.sh
@@ -5,7 +5,7 @@ Rver="${Rmaj}.4.0"
current_dir=$(pwd)
-curl -s -L http://lib.stat.cmu.edu/R/CRAN/src/base/${Rmaj}/${Rver}.tar.gz | tar xzv -C /opt
+curl -s -L http://lib.stat.cmu.edu/R/CRAN/src/base/${Rmaj}/${Rver}.tar.gz | tar xzv -C /opt || exit 1
cd /opt/${Rver}
/opt/${Rver}/configure --with-readline=no --enable-R-shlib --enable-BLAS-shlib --with-x=no || exit 1
make || exit 1
diff --git a/services/projectr/requirements.txt b/services/projectr/requirements.txt
index d1315aa9..8605072d 100644
--- a/services/projectr/requirements.txt
+++ b/services/projectr/requirements.txt
@@ -1,8 +1,6 @@
Flask==3.0.0
gunicorn==20.1.0
-rpy2==3.5.1 # 3.5.2 and up gives errors with rpy2py and py2rpy
-#rpy2==3.5.16
-#pandas==2.2.1
+rpy2==3.5.16
+pandas==2.2.1
numpy==1.26.4 # https://stackoverflow.com/a/78641304
-pandas==1.4.1
google-cloud-logging
\ No newline at end of file
diff --git a/services/projectr/rfuncs.py b/services/projectr/rfuncs.py
index 6346c922..c7e15251 100644
--- a/services/projectr/rfuncs.py
+++ b/services/projectr/rfuncs.py
@@ -16,6 +16,10 @@
from rpy2.robjects.conversion import localconverter
from rpy2.robjects.vectors import StrVector
+# If running locally, need to ensure that multiple concurrent R calls do not conflict
+from rpy2.rinterface_lib import openrlib
+
+
class RError(Exception):
"""Error based on issues that would manifest in any particular R-language call."""
def __init__(self, message="") -> None:
@@ -45,51 +49,54 @@ def run_projectR_cmd(target_df, loading_df, algorithm):
Return Pandas dataframe of the projectR output
"""
- # Convert from pandas dataframe to R data.frame
- with localconverter(ro.default_converter + pandas2ri.converter):
- target_r_df = ro.conversion.py2rpy(target_df)
- loading_r_df = ro.conversion.py2rpy(loading_df)
-
- # data.frame to matrix (projectR has no data.frame signature)
- target_r_matrix = convert_r_df_to_r_matrix(target_r_df)
- loading_r_matrix = convert_r_df_to_r_matrix(loading_r_df)
-
- # Assign Rownames to each matrix
- # I don't know why but using ro.StrVector makes rpy2py fail where the output df is an incompatible class
- # Guessing that there are some non-strings mixed into the indexes
- target_r_matrix.rownames = StrVector(target_df.index)
- loading_r_matrix.rownames = StrVector(loading_df.index)
-
- # The NMF projectR method signature is based on the LinearEmbeddedMatrix class,
- # Which has a featureLoadings property. That matrix is loaded and the default
- # projectR signature is returned and used. So we can just pass the matrix as-is.
- # https://rdrr.io/bioc/SingleCellExperiment/man/LinearEmbeddingMatrix.html
-
- # Run project R command. Get projectionPatterns matrix
- try:
- if algorithm == "nmf":
- projectR = importr('projectR')
- projection_patterns_r_matrix = projectR.projectR(data=target_r_matrix, loadings=loading_r_matrix, full=False)
- elif algorithm == "fixednmf":
- sjd = importr('SJD')
- loading_list = ro.ListVector({"genesig": loading_r_matrix})
-
- projection = sjd.projectNMF(proj_dataset=target_r_matrix, proj_group=True, list_component=loading_list)
- projection_patterns_r_matrix = projection.rx2("proj_score_list").rx2("genesig")
- else:
- raise ValueError("Algorithm {} is not supported".format(algorithm))
- except Exception as e:
- # print stacktrace with line numbers
- traceback.print_exc(file=sys.stderr)
- raise RError("Error: Could not run projectR command.\tReason: {}".format(str(e)))
-
- # matrix back to data.frame
- projection_patterns_r_df = convert_r_matrix_to_r_df(projection_patterns_r_matrix)
-
- # Convert from R data.frame to pandas dataframe
- with localconverter(ro.default_converter + pandas2ri.converter):
- projection_patterns_df = ro.conversion.rpy2py(projection_patterns_r_df)
-
- return projection_patterns_df
+ # Ensure multithreading if running locally -> https://rpy2.github.io/doc/v3.5.x/html/rinterface.html#multithreading
+ with openrlib.rlock:
+
+ # Convert from pandas dataframe to R data.frame
+ with localconverter(ro.default_converter + pandas2ri.converter):
+ target_r_df = ro.conversion.py2rpy(target_df)
+ loading_r_df = ro.conversion.py2rpy(loading_df)
+
+ # data.frame to matrix (projectR has no data.frame signature)
+ target_r_matrix = convert_r_df_to_r_matrix(target_r_df)
+ loading_r_matrix = convert_r_df_to_r_matrix(loading_r_df)
+
+ # Assign Rownames to each matrix
+ # I don't know why but using ro.StrVector makes rpy2py fail where the output df is an incompatible class
+ # Guessing that there are some non-strings mixed into the indexes
+ target_r_matrix.rownames = StrVector(target_df.index)
+ loading_r_matrix.rownames = StrVector(loading_df.index)
+
+ # The NMF projectR method signature is based on the LinearEmbeddedMatrix class,
+ # Which has a featureLoadings property. That matrix is loaded and the default
+ # projectR signature is returned and used. So we can just pass the matrix as-is.
+ # https://rdrr.io/bioc/SingleCellExperiment/man/LinearEmbeddingMatrix.html
+
+ # Run project R command. Get projectionPatterns matrix
+ try:
+ if algorithm == "nmf":
+ projectR = importr('projectR')
+ projection_patterns_r_matrix = projectR.projectR(data=target_r_matrix, loadings=loading_r_matrix, full=False)
+ elif algorithm == "fixednmf":
+ sjd = importr('SJD')
+ loading_list = ro.ListVector({"genesig": loading_r_matrix})
+
+ projection = sjd.projectNMF(proj_dataset=target_r_matrix, proj_group=True, list_component=loading_list)
+ projection_patterns_r_matrix = projection.rx2("proj_score_list").rx2("genesig")
+ else:
+ raise ValueError("Algorithm {} is not supported".format(algorithm))
+ except Exception as e:
+ # print stacktrace with line numbers
+ traceback.print_exc(file=sys.stderr)
+ raise RError("Error: Could not run projectR command.\tReason: {}".format(str(e)))
+
+ # matrix back to data.frame
+ projection_patterns_r_df = convert_r_matrix_to_r_df(projection_patterns_r_matrix)
+
+ # Convert from R data.frame to pandas dataframe
+ with localconverter(ro.default_converter + pandas2ri.converter):
+ projection_patterns_df = ro.conversion.rpy2py(projection_patterns_r_df)
+
+ return projection_patterns_df
diff --git a/www/api/resources/aggregations.py b/www/api/resources/aggregations.py
index 519f0517..ab9d66b8 100644
--- a/www/api/resources/aggregations.py
+++ b/www/api/resources/aggregations.py
@@ -50,6 +50,7 @@ def post(self, dataset_id):
ana = geardb.Analysis(id=analysis_id, dataset_id=dataset_id, session_id=session_id, user_id=user.id)
ana.discover_type()
+ adata = ana.get_adata()
else:
# Dataset is primary type
diff --git a/www/api/resources/plotly_data.py b/www/api/resources/plotly_data.py
index 14c4cbde..f90c1735 100644
--- a/www/api/resources/plotly_data.py
+++ b/www/api/resources/plotly_data.py
@@ -232,6 +232,10 @@ def post(self, dataset_id):
df = selected.to_df()
df = pd.concat([df,selected.obs], axis=1)
+ # fill any missing adata.obs values with "NA"
+ # The below line gives the error - TypeError: Cannot setitem on a Categorical with a new category (NA), set the categories first
+ #df = df.fillna("NA")
+
# Valid analysis column names from api/resources/h5ad.py
analysis_tsne_columns = ['X_tsne_1', 'X_tsne_2']
analysis_umap_columns = ['X_umap_1', 'X_umap_2']
@@ -268,6 +272,14 @@ def post(self, dataset_id):
message = "WARNING: Color map has values not in the dataframe column '{}': {}\n".format(color_name, diff)
message += "Will set color map key values to the unique values in the dataframe column."
print(message, file=sys.stderr)
+ # If any element in diff is nan and color_map contains a valid missing value key like "NA", change the value in the dataframe to match the color_map key
+ for key in list(diff):
+ if pd.isna(key) and "NA" in color_map.keys():
+ df[color_name] = df[color_name].replace({key: "NA"})
+ col_values.remove(key) # Remove the nan value from the set
+ col_values = col_values.union({"NA"})
+ break
+
# Sort both the colormap and dataframe column alphabetically
sorted_column_values = sorted(col_values)
updated_color_map = {}
diff --git a/www/api/resources/projectr.py b/www/api/resources/projectr.py
index 6063d935..5d3c2b9a 100644
--- a/www/api/resources/projectr.py
+++ b/www/api/resources/projectr.py
@@ -321,8 +321,8 @@ def projectr_callback(dataset_id, genecart_id, projection_id, session_id, scope,
# If dataset genes have duplicated index names, we need to rename them to avoid errors
# in collecting rownames in projectR (which gives invalid output)
# This means these duplicated genes will not be in the intersection of the dataset and pattern genes
+ dedup_copy = Path(ana.dataset_path().replace('.h5ad', '.dups_removed.h5ad'))
if (adata.var.index.duplicated(keep="first") == True).any():
- dedup_copy = Path(ana.dataset_path().replace('.h5ad', '.dups_removed.h5ad'))
if dedup_copy.exists():
dedup_copy.unlink()
adata = adata[:, adata.var.index.duplicated(keep="first") == False].copy(filename=dedup_copy)
@@ -497,6 +497,9 @@ def projectr_callback(dataset_id, genecart_id, projection_id, session_id, scope,
, "num_dataset_genes": num_target_genes
}
+ adata.close()
+ if dedup_copy.exists():
+ dedup_copy.unlink()
# Have had cases where the column names are x1, x2, x3, etc. so load in the original pattern names
projection_patterns_df = projection_patterns_df.set_axis(loading_df.columns, axis="columns")
diff --git a/www/api/resources/tsne_data.py b/www/api/resources/tsne_data.py
index 5dba0344..aa2e5d25 100644
--- a/www/api/resources/tsne_data.py
+++ b/www/api/resources/tsne_data.py
@@ -306,13 +306,6 @@ def post(self, dataset_id):
if flip_y:
adata.obsm[key][:,1] = -1 * adata.obsm[key][:,1]
- # We also need to change the adata's Raw var dataframe
- # We can't explicitly reset its index so we reinitialize it with
- # the newer adata object.
- # https://github.com/theislab/anndata/blob/master/anndata/base.py#L1020-L1022
- if adata.raw is not None:
- adata.raw = adata
-
# Reorder the categorical values in the observation dataframe
# Currently in UI only "plot_by_group" has reordering capabilities
if order:
@@ -363,11 +356,11 @@ def post(self, dataset_id):
# Rename to end the confusion
selected.var = selected.var.rename(columns={selected.var.columns[0]: "ensembl_id"})
# Modify the AnnData object to not include any duplicated gene symbols (keep only first entry)
+ dedup_copy = ana.dataset_path().replace('.h5ad', '.dups_removed.h5ad')
if (selected.var.index.duplicated(keep="first") == True).any():
success = 2
message = "WARNING: Multiple Ensemble IDs found for gene symbol '{}'. Using the first stored Ensembl ID.".format(selected_gene)
- dedup_copy = ana.dataset_path().replace('.h5ad', '.dups_removed.h5ad')
if os.path.exists(dedup_copy):
os.remove(dedup_copy)
selected = selected[:, selected.var.index.duplicated() == False].copy(filename=dedup_copy)
@@ -564,6 +557,9 @@ def post(self, dataset_id):
if selected.isbacked:
selected.file.close()
+ if os.path.exists(dedup_copy):
+ os.remove(dedup_copy)
+
with io.BytesIO() as io_pic:
# Set the saved figure dpi based on the number of observations in the dataset after filtering
if high_dpi:
diff --git a/www/cgi/get_analysis_image.cgi b/www/cgi/get_analysis_image.cgi
index 62d54559..557fd06f 100755
--- a/www/cgi/get_analysis_image.cgi
+++ b/www/cgi/get_analysis_image.cgi
@@ -41,11 +41,18 @@ def main():
if not image_path.startswith(ana_directory):
raise Exception("Invalid filename: {}".format(image_path))
- with open(image_path, 'rb') as f:
- print("Content-Type: image/png\n")
- sys.stdout.flush() # <---
- sys.stdout.buffer.write(f.read())
-
+ try:
+ with open(image_path, 'rb') as f:
+ print("Content-Type: image/png\n")
+ sys.stdout.flush() # <---
+ sys.stdout.buffer.write(f.read())
+ except FileNotFoundError as e:
+ print(str(e), file=sys.stderr)
+ # ensure a 404 response
+ print("Status: 404 Not Found\n")
+ print("Content-Type: text/plain\n")
+ print("File not found: {0}".format(image_path))
+ print("Error: {0}".format(e))
if __name__ == '__main__':
main()
diff --git a/www/cgi/get_stored_analysis.cgi b/www/cgi/get_stored_analysis.cgi
index 10774d56..c2beca36 100755
--- a/www/cgi/get_stored_analysis.cgi
+++ b/www/cgi/get_stored_analysis.cgi
@@ -29,8 +29,15 @@ def main():
user_id=user.id, session_id=session_id,
type=analysis_type)
- print('Content-Type: application/json\n\n')
- print(ana)
+ # try to read the analysis object and raise exception if FileNotFoundError
+
+ try:
+ print('Content-Type: application/json\n\n')
+ print(ana)
+ except FileNotFoundError:
+ print('Content-Type: application/json\n\n')
+ print('{"error": "Analysis config file not found"}')
+ return
if __name__ == '__main__':
main()
diff --git a/www/cgi/get_stored_analysis_list.cgi b/www/cgi/get_stored_analysis_list.cgi
index 368771df..66bfad16 100755
--- a/www/cgi/get_stored_analysis_list.cgi
+++ b/www/cgi/get_stored_analysis_list.cgi
@@ -35,9 +35,10 @@ def main():
result['user_unsaved'] = acollection.user_unsaved
## get the vetting for each
- for atype in result:
- for ana in result[atype]:
- ana.discover_vetting(current_user_id=user.id)
+ if user_id:
+ for atype in result:
+ for ana in result[atype]:
+ ana.discover_vetting(current_user_id=user_id)
print('Content-Type: application/json\n\n')
print(json.dumps(result))
diff --git a/www/cgi/get_user_layouts.cgi b/www/cgi/get_user_layouts.cgi
index 113b1fa6..3c26a31d 100755
--- a/www/cgi/get_user_layouts.cgi
+++ b/www/cgi/get_user_layouts.cgi
@@ -105,6 +105,14 @@ def main():
# NOTE: "null" values can happen but will be queried out in the SQL (unless there is a actual "null" share_id)
if layout_share_id:
result['shared_layouts'] = geardb.LayoutCollection(include_datasets=bool_include_members).get_by_share_id(layout_share_id)
+ # remove shared layouts that are also in the user, domain, or group layouts.
+ for l in result['shared_layouts']:
+ for ltype in ['user', 'domain', 'group', 'public']:
+ for l2 in result[ltype + '_layouts']:
+ if l.share_id == l2.share_id:
+ result['shared_layouts'].remove(l)
+ break
+
## Selected priority:
## - A passed share ID
diff --git a/www/cgi/process_uploaded_expression_dataset.cgi b/www/cgi/process_uploaded_expression_dataset.cgi
index dcbaaaad..93257a71 100755
--- a/www/cgi/process_uploaded_expression_dataset.cgi
+++ b/www/cgi/process_uploaded_expression_dataset.cgi
@@ -90,24 +90,27 @@ def main():
# This is the fork off of apache
# https://stackoverflow.com/a/22181041/1368079
# https://stackoverflow.com/questions/6024472/start-background-process-daemon-from-cgi-script
- sys.stdout = original_stdout
- result['success'] = 1
- print(json.dumps(result))
-
- sys.stdout.flush()
- os.close(sys.stdout.fileno()) # Break web pipe
- sys.stderr.flush()
- os.close(sys.stderr.fileno()) # Break web pipe
+ # https://groups.google.com/g/comp.lang.python/c/gSRnd0RoVKY?pli=1
+ do_fork = True
+ if do_fork:
+ sys.stdout = original_stdout
+ result['success'] = 1
+ print(json.dumps(result))
+
+ sys.stdout.flush()
+ sys.stdout.close()
+
+ sys.stderr.flush()
+ sys.stderr.close()
- if os.fork(): # Get out of parent process
- sys.exit(0)
+ f = os.fork()
- # open a log file in /tmp
- f_out = open('/home/jorvis/logs/apache.stdout.log', 'w')
- f_err = open('/home/jorvis/logs/apache.stderr.log', 'w')
+ if f != 0:
+ # Terminate the parent
+ sys.exit(0)
+ # PARENT DEAD
- time.sleep(1) # Be sure the parent process reach exit command.
- os.setsid() # Become process group leader
+ # CHILD CONTINUES FROM HERE
status['process_id'] = os.getpid()
diff --git a/www/js/classes/analysis-ui.js b/www/js/classes/analysis-ui.js
index 37277a09..c6ffa339 100644
--- a/www/js/classes/analysis-ui.js
+++ b/www/js/classes/analysis-ui.js
@@ -51,7 +51,7 @@ class AnalysisUI {
// Initial info
primaryInitialInfoSection = "#initial-info-s"
primaryInitialPlotContainer = "#initial-plot-c"
- primaryInitialLoadingPlotElt = "#initial-loading-plot"
+ primaryInitialLoadingElt = "#initial-loading-c"
primaryInitialScatterContainer = "#initial-scatter-c"
primaryInitialViolinContainer = "#initial-violin-c"
selectedDatasetShapeInitialElt = "#selected-dataset-shape-initial"
diff --git a/www/js/classes/analysis.js b/www/js/classes/analysis.js
index ecfc00c3..884a6acd 100644
--- a/www/js/classes/analysis.js
+++ b/www/js/classes/analysis.js
@@ -18,12 +18,12 @@ class Analysis {
label = `Unlabeled ${commonDateTime()}`,
type,
vetting,
- userSessionId = CURRENT_USER.session_id,
+ analysisSessionId = CURRENT_USER.session_id,
genesOfInterest = [],
groupLabels = []
} = {}) {
this.id = id;
- this.userSessionId = userSessionId;
+ this.analysisSessionId = analysisSessionId;
this.dataset = datasetObj; // The dataset object
this.type = type;
this.vetting = vetting;
@@ -99,7 +99,7 @@ class Analysis {
*/
async copyDatasetAnalysis(destType) {
const params = {
- session_id: this.userSessionId,
+ session_id: this.analysisSessionId,
dataset_id: this.dataset.id,
source_analysis_id: this.id,
dest_analysis_id: this.id,
@@ -131,7 +131,7 @@ class Analysis {
this.type = 'user_unsaved';
this.id = newAnalysisId;
- this.userSessionId = CURRENT_USER.session_id;
+ this.analysisSessionId = CURRENT_USER.session_id;
document.querySelector(UI.analysisActionContainer).classList.remove("is-hidden");
document.querySelector(UI.analysisStatusInfoContainer).classList.add("is-hidden");
@@ -155,7 +155,7 @@ class Analysis {
try {
const {data} = await axios.post("./cgi/delete_dataset_analysis.cgi", convertToFormData({
- session_id: this.userSessionId,
+ session_id: this.analysisSessionId,
dataset_id: this.dataset.id,
analysis_id: this.id,
analysis_type: this.type
@@ -181,7 +181,7 @@ class Analysis {
// create URL parameters
const params = {
- session_id: this.userSessionId,
+ session_id: this.analysisSessionId,
dataset_id: this.dataset.id,
analysis_id: this.id,
type: "h5ad",
@@ -202,49 +202,6 @@ class Analysis {
logErrorInConsole(error);
createToast(`Error downloading analysis h5ad`);
}
-
- }
-
- /**
- * Retrieves the stored analysis data from the server.
- * @returns {Promise} A promise that resolves when the analysis data is retrieved.
- */
- async getStoredAnalysis() {
-
- // Some dataset info (like organism ID) may be lost when loading an analysis from JSON
- const datasetObj = this.dataset;
-
- try {
- const {data} = await axios.post("./cgi/get_stored_analysis.cgi", convertToFormData({
- analysis_id: this.id,
- analysis_type: this.type,
- session_id: this.userSessionId,
- dataset_id: this.dataset.id
- }));
-
- // Load the analysis data and assign it to the current instance
- const ana = Analysis.loadFromJson(data);
- Object.assign(this, ana);
-
- // If tSNE was calculate, show the labeled tSNE section
- // Mainly for primary analyses
- // ? verify claim
- document.querySelector(UI.labeledTsneSection).classList.add("is-hidden");
- if (this.type === "primary" && data['tsne']['tsne_calculated']) {
- document.querySelector(UI.labeledTsneSection).classList.remove("is-hidden");
-
- // Initialize the labeled tSNE step
- this.labeledTsne = new AnalysisStepLabeledTsne(this);
- }
-
- } catch (error) {
- logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error}`);
- createToast(`Error getting stored analysis`);
- }
-
- // Restore the dataset object
- this.dataset = datasetObj;
-
}
/**
@@ -260,7 +217,7 @@ class Analysis {
try {
const {data} = await axios.post("./cgi/get_stored_analysis_list.cgi", convertToFormData({
dataset_id: datasetId,
- session_id: this.userSessionId
+ session_id: this.analysisSessionId
}));
// Create an empty option for the analysis select element
@@ -282,6 +239,7 @@ class Analysis {
const option = document.createElement("option");
option.dataset.analysisId = analysis.id;
option.dataset.analysisType = analysis.type;
+ option.dataset.analysisSessionId = analysis.session_id || CURRENT_USER.session_id;
option.dataset.datasetId = analysis.dataset_id;
option.textContent = analysis.label || "Unlabeled"
// ? Using standard HTML, cannot add icons to options, so making icons by vetting status is not possible
@@ -321,6 +279,15 @@ class Analysis {
// unsaved
if (data.user_unsaved.length) {
+ // if data has no label, it will be "Unlabeled"
+ const count = 1
+ data.user_unsaved.forEach(analysis => {
+ if (!analysis.label) {
+ analysis.label = `Unlabeled ${count}`;
+ count++;
+ }
+ });
+
// sort by label
data.user_unsaved.sort((a, b) => a.label.localeCompare(b.label));
@@ -334,6 +301,15 @@ class Analysis {
// saved
if (data.user_saved.length) {
+ // if data has no label, it will be "Unlabeled"
+ const count = 1
+ data.user_saved.forEach(analysis => {
+ if (!analysis.label) {
+ analysis.label = `Unlabeled ${count}`;
+ count++;
+ }
+ });
+
// sort by label
data.user_saved.sort((a, b) => a.label.localeCompare(b.label));
@@ -347,6 +323,15 @@ class Analysis {
// public
if (data.public.length) {
+ // if data has no label, it will be "Unlabeled"
+ const count = 1
+ data.public.forEach(analysis => {
+ if (!analysis.label) {
+ analysis.label = `Unlabeled ${count}`;
+ count++;
+ }
+ });
+
// sort by label
data.public.sort((a, b) => a.label.localeCompare(b.label));
@@ -368,23 +353,71 @@ class Analysis {
createToast(`Error getting saved analyses: ${error}`);
logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error}`);
}
+ }
+
+
+ /**
+ * Retrieves the stored analysis data from the server.
+ * @returns {Promise} A promise that resolves when the analysis data is retrieved.
+ */
+ async getStoredAnalysis() {
+
+ // Some dataset info (like organism ID) may be lost when loading an analysis from JSON
+ const datasetObj = this.dataset;
+
+ try {
+ const {data} = await axios.post("./cgi/get_stored_analysis.cgi", convertToFormData({
+ analysis_id: this.id,
+ analysis_type: this.type,
+ session_id: this.analysisSessionId,
+ dataset_id: this.dataset.id
+ }));
+
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ // Load the analysis data and assign it to the current instance
+ const ana = await Analysis.loadFromJson(data, datasetObj);
+ Object.assign(this, ana);
+
+ // If tSNE was calculate, show the labeled tSNE section
+ // Mainly for primary analyses
+ // ? verify claim
+ document.querySelector(UI.labeledTsneSection).classList.add("is-hidden");
+ if (this.type === "primary" && data['tsne']['tsne_calculated']) {
+ document.querySelector(UI.labeledTsneSection).classList.remove("is-hidden");
+
+ // Initialize the labeled tSNE step
+ this.labeledTsne = new AnalysisStepLabeledTsne(this);
+ }
+
+ } catch (error) {
+ logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error}`);
+ createToast(`Error retrieving stored analysis`);
+ }
+
+ // Restore the dataset object
+ this.dataset = datasetObj;
}
/**
* Loads an Analysis object from JSON data.
*
- * @param {Object} data - The JSON data representing the Analysis object.
- * @returns {Analysis} The loaded Analysis object.
+ * @param {Object} data - The JSON data representing the Analysis.
+ * @param {Object} datasetObj - The dataset object associated with the Analysis.
+ * @returns {Promise} - The loaded Analysis object.
*/
- static async loadFromJson(data) {
+ static async loadFromJson(data, datasetObj) {
+
const analysis = new Analysis({
id: data.id,
- datasetObj: data.dataset,
+ datasetObj,
datasetIsRaw: data.dataset_is_raw,
label: data.label,
type: data.type,
- userSessionId: data.user_session_id || CURRENT_USER.session_id,
+ analysisSessionId: data.session_id || CURRENT_USER.session_id,
groupLabels: data.group_labels,
genesOfInterest: data.genesOfInterest
});
@@ -443,6 +476,7 @@ class Analysis {
// Support legacy data.
const clusteringEditData = data.clustering_edit || data.clustering
+ clusteringEditData.mode = "edit";
analysis.clusteringEdit = AnalysisStepClustering.loadFromJson(clusteringEditData, analysis);
@@ -464,18 +498,16 @@ class Analysis {
* @returns {Promise} A promise that resolves when the preliminary figures are loaded.
*/
async loadPreliminaryFigures() {
+ document.querySelector(UI.primaryInitialPlotContainer).classList.add("is-hidden");
- document.querySelector(UI.primaryInitialPlotContainer).classList.remove("is-hidden");
try {
const {data} = await axios.post("./cgi/h5ad_preview_primary_filter.cgi", convertToFormData({
dataset_id: this.dataset.id,
analysis_id: this.id,
analysis_type: this.type,
- session_id: this.userSessionId
+ session_id: this.analysisSessionId
}));
- document.querySelector(UI.primaryInitialLoadingPlotElt).classList.add("is-hidden");
-
if (!data.success || data.success < 1) {
document.querySelector(UI.primaryInitialViolinContainer).textContent = "Preliminary figures not yet generated. Continue your analysis.";
createToast("Preliminary figures not found. You can still continue the analysis though.", "is-warning");
@@ -484,6 +516,7 @@ class Analysis {
document.querySelector(UI.primaryInitialViolinContainer).innerHTML = `
`;
document.querySelector(UI.primaryInitialScatterContainer).innerHTML = `
`;
createToast("Preliminary plots displayed", "is-success");
+ document.querySelector(UI.primaryInitialPlotContainer).classList.remove("is-hidden");
} catch (error) {
createToast("Failed to access dataset");
@@ -540,17 +573,18 @@ class Analysis {
*/
async placeAnalysisImage({params, title, target} = {}) {
const url = "./cgi/get_analysis_image.cgi";
- const response = await axios.get(url, { params });
- if (response.status === 200) {
- const imgSrc = response.request.responseURL;
- const html = `
`;
- document.querySelector(target).innerHTML = html;
- return;
+ try {
+ const response = await axios.get(url, { params });
+ if (response?.status === 200) {
+ const imgSrc = response.request.responseURL;
+ const html = `
`;
+ document.querySelector(target).innerHTML = html;
+ }
+ } catch (error) {
+ console.error(`Error: ${error.response?.status}`);
+ createToast(`Error retrieving analysis image for at least one completed step. You can re-run those steps to generate images again.`, "is-warning");
}
-
- console.error(`Error: ${response.status}`);
- createToast(`Error getting analysis image`);
}
/**
@@ -604,7 +638,7 @@ class Analysis {
async save() {
- if (!this.userSessionId) {
+ if (!this.analysisSessionId) {
console.warn("Cannot save analysis without a user session ID");
return;
}
@@ -638,6 +672,7 @@ class Analysis {
// Some legacy things to change around
state.dataset_id = state.dataset.id;
+ delete state.dataset; // redundant with other saved things. We can retrieve dataset info when loading
if (state.qc_by_mito) {
state.qc_by_mito.filter_mito_perc = state.qc_by_mito.filter_mito_percent;
delete state.qc_by_mito.filter_mito_percent;
@@ -645,7 +680,7 @@ class Analysis {
try {
const {data} = await axios.post("./cgi/save_dataset_analysis.cgi", convertToFormData({
- session_id: this.userSessionId,
+ session_id: this.analysisSessionId,
dataset_id: this.dataset.id,
analysis_id: this.id,
analysis_type: this.type,
@@ -690,7 +725,7 @@ class Analysis {
this.type = 'user_saved';
document.querySelector(UI.btnSaveAnalysisElt).textContent = "Saved";
document.querySelector(UI.analysisActionContainer).classList.add("is-hidden");
- createToast("This analysis is stored in your profile.", "is-info", true);
+ createToast("This analysis has been saved in your private user profile.", "is-info", true);
document.querySelector(UI.analysisStatusInfoContainer).classList.remove("is-hidden");
document.querySelector(UI.btnDeleteSavedAnalysisElt).classList.remove("is-hidden");
document.querySelector(UI.btnMakePublicCopyElt).classList.remove("is-hidden");
@@ -822,7 +857,7 @@ class AnalysisStepPrimaryFilter {
analysis_id: this.analysis.id,
analysis_type: this.analysis.type,
dataset_id: this.analysis.dataset.id,
- session_id: this.analysis.userSessionId,
+ session_id: this.analysis.analysisSessionId,
filter_cells_lt_n_genes: this.filterCellsLtNGenes || "",
filter_cells_gt_n_genes: this.filterCellsGtNGenes || "",
filter_genes_lt_n_cells: this.filterGenesLtNCells || "",
@@ -992,7 +1027,7 @@ class AnalysisStepPrimaryFilter {
'analysis_name': 'highest_expr_genes',
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
datetime: (new Date()).getTime()
}
@@ -1114,7 +1149,7 @@ class AnalysisStepQCByMito {
dataset_id: this.analysis.dataset.id,
analysis_id: this.analysis.id,
analysis_type: this.analysis.type,
- session_id: this.analysis.userSessionId,
+ session_id: this.analysis.analysisSessionId,
genes_prefix: this.genePrefix,
filter_mito_perc: this.filterMitoPercent,
filter_mito_count: this.filterMitoCount,
@@ -1180,7 +1215,7 @@ class AnalysisStepQCByMito {
'analysis_name': 'violin_qc_by_mito',
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
}
@@ -1317,7 +1352,7 @@ class AnalysisStepSelectVariableGenes {
'dataset_id': this.analysis.dataset.id,
'analysis_id': this.analysis.id,
'analysis_type': this.analysis.type,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
'norm_counts_per_cell': this.normCountsPerCell,
'flavor': this.flavor,
'n_top_genes': this.nTopGenes,
@@ -1410,7 +1445,7 @@ class AnalysisStepSelectVariableGenes {
'analysis_name': 'filter_genes_dispersion',
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
@@ -1513,7 +1548,7 @@ class AnalysisStepPCA {
dataset_id: this.analysis.dataset.id,
analysis_id: this.analysis.id,
analysis_type: this.analysis.type,
- session_id: this.analysis.userSessionId,
+ session_id: this.analysis.analysisSessionId,
genes_to_color: document.querySelector(UI.pcaGenesToColorElt).value,
compute_pca: computePCA
}));
@@ -1566,7 +1601,7 @@ class AnalysisStepPCA {
dataset_id: this.analysis.dataset.id,
analysis_id: this.analysis.id,
analysis_type: this.analysis.type,
- session_id: this.analysis.userSessionId,
+ session_id: this.analysis.analysisSessionId,
pcs: document.querySelector(UI.topPcaGenesElt).value
}));
@@ -1580,7 +1615,7 @@ class AnalysisStepPCA {
'analysis_name': 'pca_loadings',
'analysis_type': this.analysis.type,
'dataset_id': this.analysis.dataset.id,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
datetime: (new Date()).getTime()
}
@@ -1621,7 +1656,7 @@ class AnalysisStepPCA {
'analysis_name': 'pca',
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
}
@@ -1784,7 +1819,7 @@ class AnalysisSteptSNE {
'dataset_id': this.analysis.dataset.id,
'analysis_id': this.analysis.id,
'analysis_type': this.analysis.type,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
'genes_to_color': document.querySelector(UI.tsneGenesToColorElt).value,
'n_pcs': document.querySelector(UI.tsneNPcsElt).value,
'n_neighbors': document.querySelector(UI.dimReductionNNeighborsElt).value,
@@ -1811,6 +1846,8 @@ class AnalysisSteptSNE {
this.tsneCalculated = Boolean(computeTsne);
this.umapCalculated = Boolean(computeUmap);
this.genesToColor = document.querySelector(UI.tsneGenesToColorElt).value;
+ this.nNeighbors = document.querySelector(UI.dimReductionNNeighborsElt).value;
+ this.nPcs = document.querySelector(UI.tsneNPcsElt).value;
this.plotTsne = plotTsne;
this.plotUmap = plotUmap;
this.updateUIWithResults();
@@ -1874,7 +1911,7 @@ class AnalysisSteptSNE {
'analysis_name': 'tsne',
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
}
@@ -2043,7 +2080,7 @@ class AnalysisStepClustering {
dataset_id: this.analysis.dataset.id,
analysis_id: this.analysis.id,
analysis_type: this.analysis.type,
- session_id: this.analysis.userSessionId,
+ session_id: this.analysis.analysisSessionId,
resolution,
compute_clusters: computeClustering,
plot_tsne: plotTsne,
@@ -2125,7 +2162,7 @@ class AnalysisStepClustering {
'analysis_name': 'tsne_clustering',
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
}
@@ -2139,14 +2176,29 @@ class AnalysisStepClustering {
}
if (Boolean(this.plotTsne)) {
- ana.placeAnalysisImage(
- {'params': params, 'title': 'Cluster groups', 'target': tsneTarget});
+ try {
+ ana.placeAnalysisImage(
+ {'params': params, 'title': 'tSNE clustering', 'target': tsneTarget});
+ } catch (error) {
+ // legacy
+ params['analysis_name'] = 'tsne_louvain'
+ ana.placeAnalysisImage(
+ {'params': params, 'title': 'tSNE clustering', 'target': tsneTarget});
+ }
}
if (Boolean(this.plotUmap)) {
params['analysis_name'] = 'umap_clustering'
- ana.placeAnalysisImage(
- {'params': params, 'title': 'Cluster groups', 'target': umapTarget});
+ try {
+ ana.placeAnalysisImage(
+ {'params': params, 'title': 'Cluster groups', 'target': umapTarget});
+ } catch (error) {
+ // legacy
+ params['analysis_name'] = 'umap_louvain'
+ ana.placeAnalysisImage(
+ {'params': params, 'title': 'Cluster groups', 'target': umapTarget});
+ }
+
}
if (this.mode === "initial") {
@@ -2232,7 +2284,7 @@ class AnalysisStepMarkerGenes {
'dataset_id': this.analysis.dataset.id,
'analysis_id': this.analysis.id,
'analysis_type': this.analysis.type,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
'n_genes': this.nGenes,
'compute_marker_genes': this.computeMarkerGenes
}));
@@ -2277,7 +2329,7 @@ class AnalysisStepMarkerGenes {
'dataset_id': this.analysis.dataset.id,
'analysis_id': this.analysis.id,
'analysis_type': this.analysis.type,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
'marker_genes': JSON.stringify([...this.genesOfInterest])
}));
@@ -2291,7 +2343,7 @@ class AnalysisStepMarkerGenes {
'analysis_name': 'dotplot_goi',
'analysis_type': this.analysis.type,
'dataset_id': this.analysis.dataset.id,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
// this saves the user from getting a cached image each time
datetime: (new Date()).getTime()
}
@@ -2523,7 +2575,7 @@ class AnalysisStepMarkerGenes {
'analysis_name': `rank_genes_groups_${data.cluster_label}`,
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
}
@@ -2739,7 +2791,7 @@ class AnalysisStepCompareGenes {
'dataset_id': this.analysis.dataset.id,
'analysis_id': this.analysis.id,
'analysis_type': this.analysis.type,
- 'session_id': this.analysis.userSessionId,
+ 'session_id': this.analysis.analysisSessionId,
'n_genes': document.querySelector(UI.compareGenesNGenesElt).value,
'group_labels': JSON.stringify(this.analysis.groupLabels),
'query_cluster': document.querySelector(UI.queryClusterSelectElt).value,
@@ -2811,7 +2863,7 @@ class AnalysisStepCompareGenes {
'analysis_name': `rank_genes_groups_${data.cluster_label}_comp_ranked`,
'analysis_type': ana.type,
'dataset_id': ana.dataset.id,
- 'session_id': ana.userSessionId,
+ 'session_id': ana.analysisSessionId,
// this saves the user from getting a cached image each time
'datetime': (new Date()).getTime()
}
diff --git a/www/js/curator_common.js b/www/js/curator_common.js
index edcc2294..37c1f38f 100644
--- a/www/js/curator_common.js
+++ b/www/js/curator_common.js
@@ -489,14 +489,18 @@ const analysisSelectUpdate = async () => {
* @returns {Promise} - A promise that resolves when the analysis is chosen and the UI is updated.
*/
const chooseAnalysis = async () => {
- const analysisValue = analysisSelect.selectedOptions.length ? getSelect2Value(analysisSelect) : undefined;
- const analysisId = (analysisValue && analysisValue > 0) ? analysisValue : null;
- const analysisText = (analysisId?.length) ? analysisId : "Primary Analysis";
+ const analysisId = getAnalysisId()
+ const analysisText = analysisSelect.selectedOptions.length ? analysisSelect.selectedOptions[0].data.text : "Primary Analysis";
// Display current selected analysis
document.getElementById("current-analysis").textContent = analysisText;
document.getElementById("current-analysis-post").textContent = analysisText;
+ // User may have chosen a new analysis with plot options
+ document.getElementById("plot-type-select").disabled = false;
+
+ // Clear plot type options so user is forced to choose a new plot type instead of using a potentially incompatible one
+ plotTypeSelect.clear();
// NOTE: For now, we can just pass analysis id only to tSNE and be fine
// Any private dataset will belong to our user. Any public datasets can be found by the API "get_analysis" code.
@@ -511,9 +515,6 @@ const chooseAnalysis = async () => {
plotTypeSelectUpdate(analysisId)
, updateDatasetGenes(analysisId)
]);
-
- // Create facet widget
- facetWidget = await createFacetWidget(datasetId, analysisId, {});
}
}
@@ -585,7 +586,9 @@ const choosePlotType = async () => {
document.getElementById("current-plot-type").textContent = plotType;
// Create facet widget, which will refresh filters
- facetWidget = await createFacetWidget(datasetId, null, {});
+
+ const analysisId = getAnalysisId();
+ facetWidget = await createFacetWidget(datasetId, analysisId, {});
document.getElementById("facet-content").classList.remove("is-hidden");
document.getElementById("selected-facets").classList.remove("is-hidden");
@@ -645,14 +648,15 @@ const cloneDisplay = async (event, display, scope="owner") => {
// NOTE: Analysis will not be chosen if the user cannot access it (i.e. owner curation, private analysis)
}
- // plot types
+ // Set up plot types
// Read clone config to populate analysis, plot type, gnee and plot-specific options
let plotType = display.plot_type;
plotType = curatorSpecificPlotTypeAdjustments(plotType);
+ // TODO: unify with plotTypeSelectUpdate code
try {
- const availablePlotTypes = await curatorApiCallsMixin.fetchAvailablePlotTypes(datasetId, undefined, isMultigene);
+ const availablePlotTypes = await curatorApiCallsMixin.fetchAvailablePlotTypes(datasetId, analysisObj?.id, isMultigene);
for (const plotType in availablePlotTypes) {
const isAllowed = availablePlotTypes[plotType];
setPlotTypeDisabledState(plotType, isAllowed);
@@ -663,12 +667,12 @@ const cloneDisplay = async (event, display, scope="owner") => {
await choosePlotType();
// In this step, a PlotStyle object is instantiated onto "plotStyle", and we will use that
} catch (error) {
- console.error(error);
document.getElementById("plot-type-s-failed").classList.remove("is-hidden");
document.getElementById("plot-type-select-c-failed").classList.remove("is-hidden");
document.getElementById("plot-type-s-success").classList.add("is-hidden");
document.getElementById("plot-type-select-c-success").classList.add("is-hidden");
-
+ document.getElementById("plot-type-select").disabled = true;
+ plotTypeSelect.update();
return;
} finally {
cloneElt.classList.remove("is-loading");
@@ -868,6 +872,16 @@ const disableCheckboxLabel = (checkboxElt, state) => {
}
}
+/**
+ * Retrieves the analysis ID from the selected options.
+ *
+ * @returns {string|null} The analysis ID, or null if no analysis is selected.
+ */
+const getAnalysisId = () => {
+ const analysisValue = analysisSelect.selectedOptions.length ? getSelect2Value(analysisSelect) : undefined;
+ return analysisValue || null;
+}
+
/**
* Retrieves updates and additions to the plot from the plot_display_config JS object.
*
@@ -1109,12 +1123,14 @@ const plotTypeSelectUpdate = async (analysisId=null) => {
// set plot type to first option
setSelectBoxByValue("plot-type-select", "nope");
- plotTypeSelect.update();
} catch (error) {
document.getElementById("plot-type-s-failed").classList.remove("is-hidden");
document.getElementById("plot-type-select-c-failed").classList.remove("is-hidden");
document.getElementById("plot-type-s-success").classList.add("is-hidden");
- document.getElementById("plot-type-select-c-success").classList.add("is-hidden");;
+ document.getElementById("plot-type-select-c-success").classList.add("is-hidden");
+ document.getElementById("plot-type-select").disabled = true;
+ } finally {
+ plotTypeSelect.update();
}
}
diff --git a/www/js/dataset_curator.js b/www/js/dataset_curator.js
index 2b9df854..de465931 100644
--- a/www/js/dataset_curator.js
+++ b/www/js/dataset_curator.js
@@ -1100,8 +1100,7 @@ const renderColorPicker = (seriesName) => {
* @returns {Promise} A promise that resolves when the options are set up.
*/
const setupPlotlyOptions = async () => {
- const analysisValue = analysisSelect.selectedOptions.length ? getSelect2Value(analysisSelect) : undefined;
- const analysisId = (analysisValue && analysisValue > 0) ? analysisValue : null;
+ const analysisId = getAnalysisId();
const plotType = getSelect2Value(plotTypeSelect);
try {
({obs_columns: allColumns, obs_levels: levels} = await curatorApiCallsMixin.fetchH5adInfo(datasetId, analysisId));
@@ -1119,7 +1118,7 @@ const setupPlotlyOptions = async () => {
if (!allColumns.length) {
document.getElementById("plot-options-s-failed").classList.remove("is-hidden");
- createToast("No metadata columns found in dataset. Cannot create a plot. Please choose another dataset");
+ createToast("No metadata columns found in dataset. Cannot create a plot. Please choose another analysis or choose another dataset.");
return;
}
@@ -1358,8 +1357,7 @@ const setupPlotlyOptions = async () => {
* @returns {Promise} A promise that resolves when the setup is complete.
*/
const setupScanpyOptions = async () => {
- const analysisValue = analysisSelect.selectedOptions.length ? getSelect2Value(analysisSelect) : undefined;
- const analysisId = (analysisValue && analysisValue > 0) ? analysisValue : null;
+ const analysisId = getAnalysisId();
const plotType = getSelect2Value(plotTypeSelect);
try {
({obs_columns: allColumns, obs_levels: levels} = await curatorApiCallsMixin.fetchH5adInfo(datasetId, analysisId));
@@ -1378,7 +1376,7 @@ const setupScanpyOptions = async () => {
if (!allColumns.length) {
document.getElementById("plot-options-s-failed").classList.remove("is-hidden");
- createToast("No metadata columns found in dataset. Cannot create a plot. Please choose another dataset");
+ createToast("No metadata columns found in dataset. Cannot create a plot. Please choose another analysis or choose another dataset.");
return;
}
diff --git a/www/js/multigene_curator.js b/www/js/multigene_curator.js
index 2a408dd5..55f4e1f0 100644
--- a/www/js/multigene_curator.js
+++ b/www/js/multigene_curator.js
@@ -212,7 +212,7 @@ class GenesAsAxisHandler extends PlotHandler {
if (!catColumns.length) {
document.getElementById("plot-options-s-failed").classList.remove("is-hidden");
- createToast("No categorical columns found in dataset. Cannot create a plot. Please choose another dataset");
+ createToast("No metadata columns found in dataset. Cannot create a plot. Please choose another analysis or choose another dataset.");
return;
}
@@ -584,7 +584,7 @@ class GenesAsDataHandler extends PlotHandler {
if (!catColumns.length) {
document.getElementById("plot-options-s-failed").classList.remove("is-hidden");
- createToast("No categorical columns found in dataset. Cannot create a plot. Please choose another dataset");
+ createToast("No metadata columns found in dataset. Cannot create a plot. Please choose another analysis or choose another dataset.");
return;
}
@@ -873,8 +873,7 @@ const fetchDashData = async (datasetId, analysis, plotType, plotConfig) => {
}
const getCategoryColumns = async () => {
- const analysisValue = analysisSelect.selectedOptions.length ? getSelect2Value(analysisSelect) : undefined;
- const analysisId = (analysisValue && analysisValue > 0) ? analysisValue : null;
+ const analysisId = getAnalysisId();
try {
({obs_columns: allColumns, obs_levels: levels} = await curatorApiCallsMixin.fetchH5adInfo(datasetId, analysisId));
} catch (error) {
diff --git a/www/js/sc_workbench.js b/www/js/sc_workbench.js
index d2b9cb6e..f4fd4750 100644
--- a/www/js/sc_workbench.js
+++ b/www/js/sc_workbench.js
@@ -80,13 +80,28 @@ const datasetTree = new DatasetTree({
// This is a placeholder to retrieve preliminary figures which are stored in the "primary" directory
currentAnalysis = new Analysis({id: datasetId, type: "primary", datasetIsRaw: true});
- // Technically these could load asynchronously, but logically the progress logs make more sense sequentially
+ try {
+ document.querySelector(UI.analysisSelect).disabled = true;
+ analysisLabels = await currentAnalysis.getSavedAnalysesList(datasetId, -1, 'sc_workbench');
+ } catch (error) {
+ createToast("Failed to access analyses for this dataset");
+ logErrorInConsole(error);
+ }
+
+ document.querySelector(UI.primaryInitialInfoSection).classList.remove("is-hidden");
+ document.querySelector(UI.primaryInitialLoadingElt).classList.remove("is-hidden");
try {
await getDatasetInfo(datasetId);
- await currentAnalysis.loadPreliminaryFigures();
+ await currentAnalysis.loadPreliminaryFigures(); // depends on dataset.id from getDatasetInfo
+ document.querySelector(UI.analysisSelect).disabled = false;
} catch (error) {
logErrorInConsole(error);
+
+ // Cannot run analyses without a dataset
+ document.querySelector(UI.analysisSelect).disabled = true;
// pass
+ } finally {
+ document.querySelector(UI.primaryInitialLoadingElt).classList.add("is-hidden");
}
})
@@ -159,8 +174,6 @@ const downloadTableAsExcel = (tableId, filename) => {
* @returns {Promise} - A promise that resolves when the dataset information is retrieved and UI updates are complete.
*/
const getDatasetInfo = async (datasetId) => {
- document.querySelector(UI.analysisSelect).disabled = true;
-
try {
const data = await apiCallsMixin.fetchDatasetInfo(datasetId);
@@ -169,18 +182,12 @@ const getDatasetInfo = async (datasetId) => {
currentAnalysis.dataset = ds;
document.querySelector(UI.primaryFilterSection).classList.remove("is-hidden");
- analysisLabels = currentAnalysis.getSavedAnalysesList(ds.id, -1, 'sc_workbench'); // select first "selct an analysis" option
-
- document.querySelector(UI.primaryInitialInfoSection).classList.remove("is-hidden");
document.querySelector(UI.selectedDatasetShapeInitialElt).textContent = currentAnalysis.dataset.shape();
- document.querySelector(UI.analysisSelect).disabled = false;
createToast("Dataset loaded", "is-success");
} catch (error) {
createToast("Failed to access dataset");
logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error.message}`);
- document.querySelector(UI.analysisSelect).disabled = true;
-
}
}
@@ -360,7 +367,7 @@ const savePcaGeneList = async () => {
'dataset_id': currentAnalysis.dataset.id,
'analysis_id': currentAnalysis.id,
'analysis_type': currentAnalysis.type,
- 'session_id': currentAnalysis.userSessionId,
+ 'session_id': currentAnalysis.analysisSessionId,
}));
if (!data.success || data.success < 1) {
@@ -919,6 +926,7 @@ document.querySelector(UI.analysisSelect).addEventListener("change", async (even
const selectedOption = event.target.selectedOptions[0];
currentAnalysis.type = selectedOption.dataset.analysisType;
currentAnalysis.id = selectedOption.dataset.analysisId;
+ currentAnalysis.analysisSessionId = selectedOption.dataset.analysisSessionId;
await currentAnalysis.getStoredAnalysis(); // await-able
@@ -933,7 +941,7 @@ document.querySelector(UI.analysisSelect).addEventListener("change", async (even
document.querySelector(UI.analysisPrimaryNotificationElt).classList.add("is-hidden");
document.querySelector(UI.analysisActionContainer).classList.add("is-hidden");
document.querySelector(UI.analysisStatusInfoContainer).classList.remove("is-hidden");
- createToast("This analysis is stored in your profile.", "is-info", true);
+ createToast("This analysis is stored in your private user profile.", "is-info", true);
document.querySelector(UI.btnMakePublicCopyElt).classList.remove("is-hidden");
}
@@ -948,7 +956,7 @@ document.querySelector(UI.analysisSelect).addEventListener("change", async (even
document.querySelector(UI.analysisPrimaryNotificationElt).classList.add("is-hidden");
document.querySelector(UI.analysisActionContainer).classList.add("is-hidden");
document.querySelector(UI.analysisStatusInfoContainer).classList.add("is-hidden");
- createToast("Changes made to this public analysis will spawn a local copy within your profile.", "is-info", true);
+ createToast("Changes made to this public analysis will create a local private copy within your profile.", "is-info", true);
document.querySelector(UI.btnMakePublicCopyElt).classList.add("is-hidden");
}
diff --git a/www/js/stepper-fxns.js b/www/js/stepper-fxns.js
index d0b8258b..ff59a176 100644
--- a/www/js/stepper-fxns.js
+++ b/www/js/stepper-fxns.js
@@ -34,9 +34,9 @@
* @param {string} selectorHref - The href of the step to be blocked.
*/
const blockStepWithHref = (selectorHref) => {
- document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}']`).parentElement.classList.remove("is-dashed", "is-active");
+ document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}']`).parentElement.classList.remove("is-dashed", "is-active", "is-light");
document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}']`).classList.add("is-dark");
- document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}'] i`).classList.remove("mdi-check");
+ document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}'] i`).classList.remove("mdi-check", "mdi-pencil");
document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}'] i`).classList.add("mdi-lock");
}
@@ -45,9 +45,9 @@ const blockStepWithHref = (selectorHref) => {
* @param {string} selector - The CSS selector of the step element.
*/
const blockStep = (selector) => {
- document.querySelector(`.steps:not(.is-hidden) ${selector}`).parentElement.classList.remove("is-dashed", "is-active");
+ document.querySelector(`.steps:not(.is-hidden) ${selector}`).parentElement.classList.remove("is-dashed", "is-active", "is-light");
document.querySelector(`.steps:not(.is-hidden) ${selector}`).classList.add("is-dark");
- document.querySelector(`.steps:not(.is-hidden) ${selector} i`).classList.remove("mdi-check");
+ document.querySelector(`.steps:not(.is-hidden) ${selector} i`).classList.remove("mdi-check", "mdi-pencil");
document.querySelector(`.steps:not(.is-hidden) ${selector} i`).classList.add("mdi-lock");
}
diff --git a/www/sc_workbench.html b/www/sc_workbench.html
index 0c805652..ab8b0689 100644
--- a/www/sc_workbench.html
+++ b/www/sc_workbench.html
@@ -169,11 +169,11 @@
Initial shape:
+ ![Loading](../img/loading_search.gif)
Loading initial gene/cell count plots
+
Initial composition plots
-
![Loading](../img/loading_search.gif)
Loading initial gene/cell
- count plots