From cd3e9885d0401604cd6fa2df341f024da6084451 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 08:41:17 -0500 Subject: [PATCH 1/8] renaming functions to use const --- www/js/common.v2.js | 28 ++++++++++++++-------------- www/js/expression.js | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/www/js/common.v2.js b/www/js/common.v2.js index 8b862812..269cf249 100644 --- a/www/js/common.v2.js +++ b/www/js/common.v2.js @@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', () => { // Bulma dropdowns can't be clicked without a wee bit of Javascript document.querySelectorAll('.dropdown-trigger').forEach(item => { - item.addEventListener('click', function(event) { + item.addEventListener('click', (event) => { event.stopPropagation(); item.parentNode.classList.toggle('is-active'); }); @@ -47,13 +47,13 @@ document.addEventListener('DOMContentLoaded', () => { const navbarElementsToAnimate = document.querySelectorAll('.icon-text-part'); // Collapse the left navigation panel to the smaller version - function hideNavbarElementsWithAnimation() { + const hideNavbarElementsWithAnimation = () => { // Hide all the menu labels - document.querySelectorAll('span.menu-label-text').forEach(function (element) { + document.querySelectorAll('span.menu-label-text').forEach((element) => { element.classList.add('is-hidden'); }); - navbarElementsToAnimate.forEach(function (element) { + navbarElementsToAnimate.forEach((element) => { // Add the CSS class to trigger the hide animation element.classList.add('hidden-sidenavbar'); // Remove the show animation class if it was previously added @@ -72,30 +72,30 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelector("#navbar-toggler i").classList.add("mdi-arrow-collapse-right"); // Hiding the span.icon-text-part causes the menu to narrow - document.querySelectorAll('span.icon-text-part').forEach(function (element) { + document.querySelectorAll('span.icon-text-part').forEach((element) => { element.classList.add('is-hidden'); }); // activate the tooltips since the menu labels are hidden - document.querySelectorAll('span.icon-image-part').forEach(function (element) { + document.querySelectorAll('span.icon-image-part').forEach((element) => { element.classList.add('has-tooltip-right', 'has-tooltip-arrow'); }); // build the tooltips based on the values of the actual menu labels - document.querySelectorAll('span.icon-image-part').forEach(function (element) { + document.querySelectorAll('span.icon-image-part').forEach((element) => { const text = element.parentNode.querySelector('span.icon-text-part').textContent; element.setAttribute('data-tooltip', text); }); } // Expand the left navigation panel to the larger version - function showNavbarElementsWithAnimation() { + const showNavbarElementsWithAnimation = () => { // Show all the menu labels - document.querySelectorAll('span.menu-label-text').forEach(function (element) { + document.querySelectorAll('span.menu-label-text').forEach((element) => { element.classList.remove('is-hidden'); }); - navbarElementsToAnimate.forEach(function (element) { + navbarElementsToAnimate.forEach((element) => { // Add the CSS class to trigger the show animation element.classList.add('shown-sidenavbar'); // Remove the hide animation class if it was previously added @@ -114,17 +114,17 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelector("#navbar-toggler i").classList.add("mdi-arrow-collapse-left"); // Hiding the span.icon-text-part causes the menu to narrow - document.querySelectorAll('span.icon-text-part').forEach(function (element) { + document.querySelectorAll('span.icon-text-part').forEach((element) => { element.classList.remove('is-hidden'); }); // hide the tooltips since the menu labels are visible - document.querySelectorAll('span.icon-image-part').forEach(function (element) { + document.querySelectorAll('span.icon-image-part').forEach((element) => { element.classList.remove('has-tooltip-right', 'has-tooltip-arrow'); }); // remove the menu tooltips since the actual labels are visible - document.querySelectorAll('span.icon-image-part').forEach(function (element) { + document.querySelectorAll('span.icon-image-part').forEach((element) => { element.removeAttribute('data-tooltip'); }); } @@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', () => { // if URL is: http://dummy.com/?technology=jquery&blog=jquerybyexample // then: var tech = getUrlParameter('technology'); // var blog = getUrlParameter('blog'); -const getUrlParameter = function getUrlParameter(sParam) { +const getUrlParameter = (sParam) => { const sPageURL = decodeURIComponent(window.location.search.substring(1)); const sURLVariables = sPageURL.split('&'); let sParameterName; diff --git a/www/js/expression.js b/www/js/expression.js index 4ecfa135..86a782ea 100644 --- a/www/js/expression.js +++ b/www/js/expression.js @@ -55,7 +55,7 @@ document.addEventListener('DOMContentLoaded', () => { } try { - await Promise.allSettled([fetchGeneAnnotations(),setupTileGrid(selected_dc_share_id)]); + await Promise.allSettled([fetchGeneAnnotations(), setupTileGrid(selected_dc_share_id)]); } catch (error) { logErrorInConsole(error); } From e437c4dadf7b81769b901e0d18ae179484a2eb79 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 08:43:27 -0500 Subject: [PATCH 2/8] renaming used selector for header in various pages --- www/include/primary_nav.html | 4 ++-- www/js/compare_datasets.js | 2 +- www/js/dataset_curator.js | 2 +- www/js/expression.js | 26 ++++++++++++++++++++++++-- www/js/multigene_curator.js | 2 +- www/js/plot_display_config.v2.js | 2 +- www/js/user_profile.js | 2 +- 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/www/include/primary_nav.html b/www/include/primary_nav.html index 43367170..2bca9af6 100644 --- a/www/include/primary_nav.html +++ b/www/include/primary_nav.html @@ -27,7 +27,7 @@ diff --git a/www/js/compare_datasets.js b/www/js/compare_datasets.js index 3bb4edef..74f02b3e 100644 --- a/www/js/compare_datasets.js +++ b/www/js/compare_datasets.js @@ -1395,7 +1395,7 @@ document.getElementById("download_selected_genes_btn").addEventListener("click", const handlePageSpecificLoginUIUpdates = async (event) => { // Update with current page info - document.querySelector("#header_bar .navbar-item").textContent = "Comparison Tool"; + document.getElementById("page-header-label").textContent = "Comparison Tool"; for (const elt of document.querySelectorAll("#primary_nav .menu-list a.is-active")) { elt.classList.remove("is-active"); } diff --git a/www/js/dataset_curator.js b/www/js/dataset_curator.js index 809a3a67..8f2f7ca8 100644 --- a/www/js/dataset_curator.js +++ b/www/js/dataset_curator.js @@ -806,7 +806,7 @@ const curatorSpecifcDatasetTreeCallback = () => { * Updates the curator-specific navbar with the current page information. */ const curatorSpecificNavbarUpdates = () => { - document.querySelector("#header_bar .navbar-item").textContent = "Single-gene Curator"; + document.getElementById("page-header-label").textContent = "Single-gene Curator"; for (const elt of document.querySelectorAll("#primary_nav .menu-list a.is-active")) { elt.classList.remove("is-active"); diff --git a/www/js/expression.js b/www/js/expression.js index 86a782ea..2909fd8c 100644 --- a/www/js/expression.js +++ b/www/js/expression.js @@ -6,10 +6,19 @@ let currently_selected_org_id = ""; let is_multigene = false; let annotation_data = null; let manually_entered_genes = []; +let tilegrid = null; document.addEventListener('DOMContentLoaded', () => { // Set the page header title - document.querySelector('#page-header-label').textContent = 'Gene Expression Search'; + document.getElementById('page-header-label').textContent = 'Gene Expression Search'; + + // Set current sidebar menu item to active + for (const elt of document.querySelectorAll("#primary_nav .menu-list a.is-active")) { + elt.classList.remove("is-active"); + } + + document.querySelector("a[tool='search_expression'").classList.add("is-active"); + // handle when the dropdown-gene-list-search-input input box is changed document.querySelector('#genes-manually-entered').addEventListener('change', (event) => { @@ -54,8 +63,19 @@ document.addEventListener('DOMContentLoaded', () => { return; } + // If multigene toggle changed, but genes and layout are the same, just render the grid + const new_is_multigene = document.querySelector('#single-multi-multi').checked; + if (new_is_multigene !== is_multigene && selected_dc_share_id === selected_dc_share_id) { + is_multigene = new_is_multigene; + if (tilegrid) { + tilegrid.applyTileGrid(is_multigene); + await tilegrid.renderDisplays(selected_genes, is_multigene); + return; + } + } + try { - await Promise.allSettled([fetchGeneAnnotations(), setupTileGrid(selected_dc_share_id)]); + ([undefined, tilegrid] = await Promise.allSettled([fetchGeneAnnotations(), setupTileGrid(selected_dc_share_id)])); } catch (error) { logErrorInConsole(error); } @@ -244,6 +264,8 @@ const setupTileGrid = async (layout_share_id) => { await tilegrid.renderDisplays(selected_genes, is_multigene); } catch (error) { logErrorInConsole(error); + } finally { + return tilegrid; } } diff --git a/www/js/multigene_curator.js b/www/js/multigene_curator.js index c08202c4..6f01d6a9 100644 --- a/www/js/multigene_curator.js +++ b/www/js/multigene_curator.js @@ -759,7 +759,7 @@ const curatorSpecifcDatasetTreeCallback = async () => { const curatorSpecificNavbarUpdates = () => { // Update with current page info - document.querySelector("#header_bar .navbar-item").textContent = "Multi-gene Displays"; + document.getElementById("page-header-label").textContent = "Multi-gene Displays"; for (const elt of document.querySelectorAll("#primary_nav .menu-list a.is-active")) { elt.classList.remove("is-active"); diff --git a/www/js/plot_display_config.v2.js b/www/js/plot_display_config.v2.js index 1ee31017..e6900c68 100644 --- a/www/js/plot_display_config.v2.js +++ b/www/js/plot_display_config.v2.js @@ -102,7 +102,7 @@ const postPlotlyConfig = { , config: { showLink: false , displaylogo: false - , responsive: false + , responsive: true , modeBarButtonsToRemove: [ "zoom2d", "autoScale2d", diff --git a/www/js/user_profile.js b/www/js/user_profile.js index 4ca0956f..73de0a50 100644 --- a/www/js/user_profile.js +++ b/www/js/user_profile.js @@ -77,7 +77,7 @@ document.getElementById("submit_preferences").addEventListener("click", async (e const handlePageSpecificLoginUIUpdates = async (event) => { // User settings has no "active" state for the sidebar - document.querySelector("#header_bar .navbar-item").textContent = "User Profile"; + document.getElementById("page-header-label").textContent = "User Profile"; for (const elt of document.querySelectorAll("#primary_nav .menu-list a.is-active")) { elt.classList.remove("is-active"); } From 2bd2f1b1ccc90cde98217d94a78227056265f571 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 09:58:56 -0500 Subject: [PATCH 3/8] multigene curations work. Refectoring --- www/js/classes/tilegrid.js | 162 +++++++++++++------------------------ www/js/expression.js | 23 +++--- 2 files changed, 68 insertions(+), 117 deletions(-) diff --git a/www/js/classes/tilegrid.js b/www/js/classes/tilegrid.js index 89eedb3e..aca34492 100644 --- a/www/js/classes/tilegrid.js +++ b/www/js/classes/tilegrid.js @@ -18,9 +18,7 @@ class TileGrid { this.maxCols = 12 // highest number of columns in a row this.maxRows = 12 // highest number of rows in a column - this.singleGeneTiles = []; // collection of DatasetTile objects - this.multiGeneTiles = []; - + this.tiles = []; this.tilegrid = [] // this.generateTileGrid(); this.selector = selector; @@ -28,6 +26,10 @@ class TileGrid { } + /** + * Adds all displays to the tile grid. + * @returns {Promise} A promise that resolves when all displays are added. + */ async addAllDisplays() { for (const dataset of this.layout) { @@ -40,18 +42,20 @@ class TileGrid { } } + /** + * Adds default displays to all tiles in the tile grid. + * @returns {Promise} A promise that resolves when all default displays have been added. + */ async addDefaultDisplays() { - await Promise.allSettled(this.singleGeneTiles.map( async tile => await tile.addDefaultDisplay())); - await Promise.allSettled(this.multiGeneTiles.map( async tile => await tile.addDefaultDisplay())); + // Each tile has "single" or "multi" type stored, so we can use that to determine the correct default display + await Promise.allSettled(this.tiles.map( async tile => await tile.addDefaultDisplay())); } /** - * Applies the tile grid layout to the specified element. - * - * @param {boolean} [isMulti=false] - Indicates whether the multi-tile grid should be used. + * Applies the tile grid to the specified selector element. */ - applyTileGrid(isMulti = false) { - const tilegrid = isMulti ? this.tilegrid.multi : this.tilegrid.single; + applyTileGrid() { + const tilegrid = this.tilegrid; const selector = this.selector; // Clear selector element @@ -94,105 +98,26 @@ class TileGrid { } } - /** - * Generates a tile grid. - * @returns {Object} The generated tile grid object. - */ - generateTileGrid() { - const tilegrid = {}; - - tilegrid.single = this.generateSingleTileGrid(); - tilegrid.multi = this.generateMultiTileGrid(); - return tilegrid; - } /** - * Generates a multi-gene tile grid based on the layout. - * @returns {Array>} The generated tile grid. + * Generates the tile grid based on the layout and dataset information. + * + * @param {boolean} is_multigene - Indicates whether the grid is for a multigene view. */ - generateMultiTileGrid() { - const tilegrid = []; - const tiles = []; - - for (const dataset of this.layout) { - const datasetTile = new DatasetTile(dataset, true); - tiles.push(datasetTile); - } - - // sort by grid position - tiles.sort((a, b) => a.dataset.grid_position - b.dataset.grid_position); - - this.multiGeneTiles = tiles; - - for (const datasetTile of tiles) { - if (datasetTile.used) { - continue; - } - - const width = datasetTile.tile.width; - const height = datasetTile.tile.height; - - if (width === 12) { - // tile spans the entire row - const tileRow = []; - tileRow.push(datasetTile); - tilegrid.push(tileRow); - datasetTile.used = true; - continue; - } - - // tile does not span the entire row - const tileRow = []; - datasetTile.used = true; - const usedTiles = [datasetTile]; - - let remainingWidth = 12 - width; - - // find tiles that fit into the remaining width - while (remainingWidth > 0) { - const tile = tiles.find((t) => !t.used && t.tile.width <= remainingWidth); - if (!tile) { - break; - } + generateTileGrid(is_multigene = false) { - tile.used = true; - usedTiles.push(tile); - remainingWidth -= tile.tile.width; - } - - tileRow.push(...usedTiles); - tilegrid.push(tileRow); - - // check if all tiles are the same height - const allSameHeight = usedTiles.every((t) => t.tile.height === height); - if (allSameHeight) { - // all tiles are the same height - //tileRow.push(...usedTiles); - //tilegrid.push(tileRow); - } - } - - return tilegrid; - } - - /** - * Generates a single-gene tile grid based on the layout. - * @returns {Array>} The generated tile grid. - */ - generateSingleTileGrid() { - const tilegrid = []; const tiles = []; + const tilegrid = []; for (const dataset of this.layout) { - const datasetTile = new DatasetTile(dataset, false); + const datasetTile = new DatasetTile(dataset, is_multigene); tiles.push(datasetTile); } // sort by grid position tiles.sort((a, b) => a.dataset.grid_position - b.dataset.grid_position); - this.singleGeneTiles = tiles; - + this.tiles = tiles; for (const datasetTile of tiles) { if (datasetTile.used) { @@ -242,7 +167,7 @@ class TileGrid { } } - return tilegrid;; + this.tilegrid = tilegrid; // TODO: Create a subgrid for variable heights } @@ -253,13 +178,12 @@ class TileGrid { } if (isMultigene) { - await Promise.allSettled(this.multiGeneTiles.map( async tile => await tile.renderDisplay(geneSymbols))); + await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbols))); } else { const geneSymbol = Array.isArray(geneSymbols) ? geneSymbols[0] : geneSymbols; - await Promise.allSettled(this.singleGeneTiles.map( async tile => await tile.renderDisplay(geneSymbol))); + await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbol))); } - } }; @@ -274,6 +198,10 @@ class DatasetTile { this.tile.html = this.generateTileHTML(); } + /** + * Adds a default display to the tile grid. + * @returns {Promise} A promise that resolves when the default display is added. + */ async addDefaultDisplay() { const dataset = this.dataset; @@ -362,6 +290,18 @@ class DatasetTile { // if the display config was not found, then do not render if (!userDisplay && !ownerDisplay) { console.warn(`Display config for dataset ${this.dataset.id} was not found.`) + // Let the user know that the display config was not found + const cardContent = document.querySelector(`#tile_${this.tile.tile_id} .card-image`); + cardContent.replaceChildren(); + const warningMessage = document.createElement("p"); + warningMessage.classList.add("has-text-warning-dark", "has-background-warning-light", "p-2", "m-2", "has-text-weight-bold"); + // Add 200 px height and center vertically + warningMessage.style.height = "200px"; + warningMessage.style.display = "flex"; + warningMessage.style.alignItems = "center"; + warningMessage.style.justifyContent = "center"; + warningMessage.textContent = `This dataset has no viewable curations for this view. Create a new curation in the ${this.type === "single" ? "Single-gene" : "Multi-gene"} Curator to view this dataset.`; + cardContent.append(warningMessage); return; } @@ -398,8 +338,19 @@ class DatasetTile { throw new Error(`Display config for dataset ${this.dataset.id} has an invalid plot type ${display.plot_type}.`); } } catch (error) { - console.error(error); // we want to ensure other plots load even if one fails + console.error(error); + // Fill in card-image with error message + cardContent.replaceChildren(); + const errorMessage = document.createElement("p"); + errorMessage.classList.add("has-text-danger-dark", "has-background-danger-light", "p-2", "m-2", "has-text-weight-bold"); + // Add 200 px height and center vertically + errorMessage.style.height = "200px"; + errorMessage.style.display = "flex"; + errorMessage.style.alignItems = "center"; + errorMessage.style.justifyContent = "center"; + errorMessage.textContent = error.message; + cardContent.append(errorMessage); } finally { cardContent.classList.remove("is-loading"); } @@ -450,9 +401,12 @@ class DatasetTile { const custonLayout = getPlotlyDisplayUpdates(expressionDisplayConf, this.plotType, "layout") Plotly.relayout(plotlyPreview.id , custonLayout) - document.getElementById("legend_title_container").classList.remove("is-hidden"); - if (plotType === "dotplot") { - document.getElementById("legend_title_container").classList.add("is-hidden"); + const legendTitle = document.getElementById("legend_title_container"); + if (legendTitle) { + legendTitle.classList.remove("is-hidden"); + if (plotType === "dotplot") { + legendTitle.classList.add("is-hidden"); + } } } diff --git a/www/js/expression.js b/www/js/expression.js index 2909fd8c..12e7e9c6 100644 --- a/www/js/expression.js +++ b/www/js/expression.js @@ -63,19 +63,12 @@ document.addEventListener('DOMContentLoaded', () => { return; } - // If multigene toggle changed, but genes and layout are the same, just render the grid - const new_is_multigene = document.querySelector('#single-multi-multi').checked; - if (new_is_multigene !== is_multigene && selected_dc_share_id === selected_dc_share_id) { - is_multigene = new_is_multigene; - if (tilegrid) { - tilegrid.applyTileGrid(is_multigene); - await tilegrid.renderDisplays(selected_genes, is_multigene); - return; - } - } + // update multigene/single gene + is_multigene = document.querySelector('#single-multi-multi').checked; try { - ([undefined, tilegrid] = await Promise.allSettled([fetchGeneAnnotations(), setupTileGrid(selected_dc_share_id)])); + const [annotRes, tilegridRes] = await Promise.allSettled([fetchGeneAnnotations(), setupTileGrid(selected_dc_share_id)]); + tilegrid = tilegridRes.value; } catch (error) { logErrorInConsole(error); } @@ -114,6 +107,8 @@ document.addEventListener('DOMContentLoaded', () => { }); const fetchGeneAnnotations = async (callback) => { + // ! - SAdkins note - Need to either hide this if is_multigene selected, or disable the "click" event (otherwise displays will be rendered with selected gene) + try { annotation_data = await apiCallsMixin.fetchGeneAnnotations( selected_genes.join(','), @@ -250,14 +245,16 @@ const selectGeneResult = (gene_symbol) => { } // Other things can be called next, such as plotting calls + if (tilegrid) { + tilegrid.renderDisplays(currently_selected_gene_symbol, is_multigene); + } } const setupTileGrid = async (layout_share_id) => { - console.log("Setting up tile grid with layout share ID " + layout_share_id); const tilegrid = new TileGrid(layout_share_id, "#result-panel-grid"); try { tilegrid.layout = await tilegrid.getLayout(); - tilegrid.tilegrid = tilegrid.generateTileGrid(); + tilegrid.generateTileGrid(is_multigene); tilegrid.applyTileGrid(is_multigene); await tilegrid.addAllDisplays(); await tilegrid.addDefaultDisplays(); From df13eb49bfe59a2943c2bd4c55276c609cd90d60 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 11:29:14 -0500 Subject: [PATCH 4/8] fixing issues with orthology in multigene views --- lib/gear/mg_plotting.py | 13 ++------ lib/gear/orthology.py | 4 +-- www/api/resources/multigene_dash_data.py | 38 ++++++++++++++---------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/gear/mg_plotting.py b/lib/gear/mg_plotting.py index b16dcb9f..4552e685 100644 --- a/lib/gear/mg_plotting.py +++ b/lib/gear/mg_plotting.py @@ -1074,7 +1074,7 @@ def build_obs_group_indexes(df, filters, clusterbar_fields): filter_indexes[k][elem] = obs_index.tolist() return filter_indexes -def create_dataframe_gene_mask(df, gene_symbols, mapped_gene_symbols={}): +def create_dataframe_gene_mask(df, gene_symbols): """Create a gene mask to filter a dataframe.""" if not "gene_symbol" in df: raise PlotError('Missing gene_symbol column in adata.var') @@ -1120,15 +1120,8 @@ def create_dataframe_gene_mask(df, gene_symbols, mapped_gene_symbols={}): # Note to user which genes were not found in the dataset genes_not_present = [gene for gene in gene_symbols if gene not in found_genes] if genes_not_present: - # attempt to map the gene to an ensembl id, and add to the gene_filter - for gene in genes_not_present: - if gene in mapped_gene_symbols: - gene_filter = gene_filter | df['gene_symbol'].isin([mapped_gene_symbols[gene]]) - # If the gene is still not present, add it to the message - genes_not_present = [gene for gene in genes_not_present if gene not in mapped_gene_symbols] - if genes_not_present: - success = 2, - message_list.append('
  • One or more genes were not found in the dataset nor could be mapped: {}
  • '.format(', '.join(genes_not_present))) + success = 2, + message_list.append('
  • One or more genes were not found in the dataset nor could be mapped: {}
  • '.format(', '.join(genes_not_present))) message = "\n".join(message_list) return gene_filter, success, message except PlotError as pe: diff --git a/lib/gear/orthology.py b/lib/gear/orthology.py index c2698298..5559e7d4 100644 --- a/lib/gear/orthology.py +++ b/lib/gear/orthology.py @@ -156,7 +156,7 @@ def map_single_gene(gene_symbol:str, orthomap_file: Path): # Read HDF5 file using Pandas read_hdf gene_symbol_dict = create_orthology_gene_symbol_dict(orthomap_file) # NOTE: Not all genes can be mapped. Unmappable genes do not change in the original dataframe. - return gene_symbol_dict[gene_symbol] + return gene_symbol_dict.get(gene_symbol, None) def map_multiple_genes(gene_symbols:list, orthomap_file: Path): """ @@ -173,4 +173,4 @@ def map_multiple_genes(gene_symbols:list, orthomap_file: Path): gene_symbol_dict = create_orthology_gene_symbol_dict(orthomap_file) # NOTE: Not all genes can be mapped. Unmappable genes do not change in the original dataframe. - return { gene_symbol: gene_symbol_dict[gene_symbol] for gene_symbol in gene_symbols} \ No newline at end of file + return { gene_symbol: gene_symbol_dict[gene_symbol] for gene_symbol in gene_symbols if gene_symbol in gene_symbol_dict} \ No newline at end of file diff --git a/www/api/resources/multigene_dash_data.py b/www/api/resources/multigene_dash_data.py index 524b978e..dfbf8727 100644 --- a/www/api/resources/multigene_dash_data.py +++ b/www/api/resources/multigene_dash_data.py @@ -83,7 +83,10 @@ def get_mapped_gene_symbols(gene_symbols, gene_organism_id, dataset_organism_id) else: for ortholog_file in get_ortholog_files_from_dataset(dataset_organism_id, "ensembl"): try: - return map_multiple_genes(gene_symbols, ortholog_file) + mapped_gene_symbols_dict = map_multiple_genes(gene_symbols, ortholog_file) + # ? Should we check all and return the dict with the most matches + if len(mapped_gene_symbols_dict): + return mapped_gene_symbols_dict except: continue return {} @@ -296,25 +299,32 @@ def post(self, dataset_id): dataset_organism_id = dataset.organism_id mapped_gene_symbols_dict = {} + # create list of mapped_gene_symbols in gene_symbols order + mapped_gene_symbols = [] # If any searched gene is not in the dataset, attempt to map it to the dataset organism if not check_all_genes_in_dataset(adata, gene_symbols): try: mapped_gene_symbols_dict = get_mapped_gene_symbols(gene_symbols, gene_organism_id, dataset_organism_id) + if len(mapped_gene_symbols_dict): + for gene_symbol in gene_symbols: + mapped_gene_symbols.append(mapped_gene_symbols_dict.get(gene_symbol, gene_symbol)) + except: return {"success": -1, "message": "The searched gene symbols could not be mapped to the dataset organism."} + selected_gene_symbols = gene_symbols if not mapped_gene_symbols else mapped_gene_symbols # TODO: How to deal with a gene mapping to multiple Ensemble IDs try: - if not gene_symbols and plot_type in ["dotplot", "heatmap", "mg_violin"]: + if not selected_gene_symbols and plot_type in ["dotplot", "heatmap", "mg_violin"]: raise PlotError('Must pass in some genes before creating a plot of type {}'.format(plot_type)) - if len(gene_symbols) == 1 and plot_type == "heatmap": + if len(selected_gene_symbols) == 1 and plot_type == "heatmap": raise PlotError('Heatmaps require 2 or more genes as input') # Some datasets have multiple ensemble IDs mapped to the same gene. # Drop dups to prevent out-of-bounds index errors downstream - gene_filter, success, message = mg.create_dataframe_gene_mask(adata.var, gene_symbols, mapped_gene_symbols_dict) + gene_filter, success, message = mg.create_dataframe_gene_mask(adata.var, selected_gene_symbols) except PlotError as pe: return { 'success': -1, @@ -345,7 +355,7 @@ def post(self, dataset_id): # Collect all genes from the unfiltered dataset dataset_genes = adata.var['gene_symbol'].unique().tolist() # Gene symbols list may have genes not in the dataset. - normalized_genes_list, _found_genes = mg.normalize_searched_genes(dataset_genes, gene_symbols) + normalized_genes_list, _found_genes = mg.normalize_searched_genes(dataset_genes, selected_gene_symbols) # Sort ensembl IDs based on the gene symbol order sorted_ensm = map(lambda x: gene_to_ensm[x], normalized_genes_list) @@ -444,16 +454,16 @@ def post(self, dataset_id): mg.modify_volcano_plot(fig, query_val, ref_val, ensm2genesymbol, downcolor, upcolor) - if gene_symbols: + if selected_gene_symbols: dataset_genes = df['gene_symbol'].unique().tolist() - normalized_genes_list, _found_genes = mg.normalize_searched_genes(dataset_genes, gene_symbols) + normalized_genes_list, _found_genes = mg.normalize_searched_genes(dataset_genes, selected_gene_symbols) mg.add_gene_annotations_to_volcano_plot(fig, normalized_genes_list, annotate_nonsignificant) elif plot_type == "quadrant": # Get list of normalized genes before dataframe filtering takes place - if gene_symbols: + if selected_gene_symbols: dataset_genes = adata.var['gene_symbol'].unique().tolist() - normalized_genes_list, _found_genes = mg.normalize_searched_genes(dataset_genes, gene_symbols) + normalized_genes_list, _found_genes = mg.normalize_searched_genes(dataset_genes, selected_gene_symbols) try: key, control_val, compare1_val, compare2_val = mg.validate_quadrant_conditions(ref_condition, compare_group1, compare_group2) df = mg.prep_quadrant_dataframe(selected @@ -477,7 +487,7 @@ def post(self, dataset_id): fig = mg.create_quadrant_plot(df, control_val, compare1_val, compare2_val, colorscale) # Annotate selected genes - if gene_symbols: + if selected_gene_symbols: genes_not_found, genes_none_none = mg.add_gene_annotations_to_quadrant_plot(fig, normalized_genes_list) if genes_not_found: success = 2 @@ -520,6 +530,9 @@ def post(self, dataset_id): groupby = ["gene_symbol"] groupby.extend(groupby_filters) + # drop Ensembl ID index since it may not aggregate and throw warnings + df.drop(columns=[var_index], inplace=True) + grouped = df.groupby(groupby) df = grouped.agg(['mean', 'count', ('percent', percent)]) \ .fillna(0) \ @@ -753,11 +766,6 @@ def post(self, dataset_id): plot_json = json.dumps(fig, cls=PlotlyJSONEncoder) - # create list of mapped_gene_symbols in gene_symbols order - mapped_gene_symbols = [] - for gene_symbol in gene_symbols: - mapped_gene_symbols.append(mapped_gene_symbols_dict.get(gene_symbol, gene_symbol)) - # NOTE: With volcano plots, the Chrome "devtools" cannot load the JSON response occasionally return { "success": success From 8611e675e7942edc280fc27a61f9d27b4fae7f66 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 11:35:23 -0500 Subject: [PATCH 5/8] fixing orthology issues in single-gene --- www/api/resources/plotly_data.py | 4 +++- www/api/resources/svg_data.py | 4 +++- www/api/resources/tsne_data.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/www/api/resources/plotly_data.py b/www/api/resources/plotly_data.py index 1e89a82c..a69f9158 100644 --- a/www/api/resources/plotly_data.py +++ b/www/api/resources/plotly_data.py @@ -43,7 +43,9 @@ def get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id): else: for ortholog_file in get_ortholog_files_from_dataset(dataset_organism_id, "ensembl"): try: - return map_single_gene(gene_symbol, ortholog_file) + mapped_gene = map_single_gene(gene_symbol, ortholog_file) + if mapped_gene: + return mapped_gene except: continue return None diff --git a/www/api/resources/svg_data.py b/www/api/resources/svg_data.py index 25f4b9b5..cc0a8be2 100644 --- a/www/api/resources/svg_data.py +++ b/www/api/resources/svg_data.py @@ -40,7 +40,9 @@ def get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id): else: for ortholog_file in get_ortholog_files_from_dataset(dataset_organism_id, "ensembl"): try: - return map_single_gene(gene_symbol, ortholog_file) + mapped_gene = map_single_gene(gene_symbol, ortholog_file) + if mapped_gene: + return mapped_gene except: continue return None diff --git a/www/api/resources/tsne_data.py b/www/api/resources/tsne_data.py index 64801453..2909a4e6 100644 --- a/www/api/resources/tsne_data.py +++ b/www/api/resources/tsne_data.py @@ -64,7 +64,9 @@ def get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id): else: for ortholog_file in get_ortholog_files_from_dataset(dataset_organism_id, "ensembl"): try: - return map_single_gene(gene_symbol, ortholog_file) + mapped_gene = map_single_gene(gene_symbol, ortholog_file) + if mapped_gene: + return mapped_gene except: continue return None @@ -81,8 +83,6 @@ def check_gene_in_dataset(adata, gene_symbols): bool: True if any of the gene symbols are present in the dataset, False otherwise. """ gene_filter = adata.var.gene_symbol.isin(gene_symbols) - print(gene_symbols, file=sys.stderr) - print(gene_filter.any(), file=sys.stderr) return gene_filter.any() def get_analysis(analysis, dataset_id, session_id): From 4679b15d478b3209d5649c4badeec04b63771121 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 11:35:48 -0500 Subject: [PATCH 6/8] adding commented out code to unify card-header height across cards --- www/js/classes/tilegrid.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/www/js/classes/tilegrid.js b/www/js/classes/tilegrid.js index aca34492..849b615d 100644 --- a/www/js/classes/tilegrid.js +++ b/www/js/classes/tilegrid.js @@ -81,6 +81,12 @@ class TileGrid { } document.querySelector(selector).append(tilegridHTML); } + + // Make all card-header titles the same height + //const cardHeaderTitles = document.querySelectorAll(`${selector} .card-header`); + //const cardHeaderTitleHeight = Math.max(...Array.from(cardHeaderTitles).map(e => e.offsetHeight)); + //cardHeaderTitles.forEach(e => e.style.height = `${cardHeaderTitleHeight}px`); + } // NOTE: This may change if data is returned previously and can be loaded @@ -177,13 +183,20 @@ class TileGrid { throw new Error("Gene symbol or symbols are required to render displays."); } - if (isMultigene) { - await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbols))); - } else { - const geneSymbol = Array.isArray(geneSymbols) ? geneSymbols[0] : geneSymbols; - await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbol))); + const geneSymbolInput = geneSymbols; + if (!isMultigene) { + geneSymbolInput = Array.isArray(geneSymbols) ? geneSymbols[0] : geneSymbols; } + + if (!isMultigene) { + await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbolInput))); + } else { + // Sometimes multigene fails to render due to OOM errors, so we want to try each tile individually + for (const tile of this.tiles) { + await tile.renderDisplay(geneSymbolInput); + } + } } }; @@ -246,7 +259,7 @@ class DatasetTile { const cardHeader = document.createElement('div'); cardHeader.classList.add('card-header', 'has-background-primary'); const cardHeaderTitle = document.createElement('div'); - cardHeaderTitle.classList.add('card-header-title'); + cardHeaderTitle.classList.add('card-header-title', "py-0"); cardHeaderTitle.textContent = tile.title; const cardHeaderIcon = document.createElement('div'); From 6effba6262e7e37d5f1eab74005f533832d839ae Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 12:23:54 -0500 Subject: [PATCH 7/8] orthology tweaks for case insensitivity --- www/api/resources/plotly_data.py | 27 ++++++++++++++++++++------- www/api/resources/svg_data.py | 24 ++++++++++++++++++------ www/api/resources/tsne_data.py | 24 ++++++++++++++++++------ www/js/classes/tilegrid.js | 13 +++++-------- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/www/api/resources/plotly_data.py b/www/api/resources/plotly_data.py index a69f9158..592df006 100644 --- a/www/api/resources/plotly_data.py +++ b/www/api/resources/plotly_data.py @@ -25,6 +25,13 @@ abs_path_www = Path(__file__).resolve().parents[TWO_LEVELS_UP] # web-root dir PROJECTIONS_BASE_DIR = abs_path_www.joinpath('projections') +def normalize_searched_gene(gene_list, chosen_gene): + """Convert to case-insensitive version of gene. Returns None if gene not found in dataset.""" + for g in gene_list: + if chosen_gene.lower() == str(g).lower(): + return g + return None + def get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id): """ Maps a gene symbol to its corresponding orthologous gene symbol in a given dataset. @@ -287,16 +294,22 @@ def post(self, dataset_id): if not check_gene_in_dataset(adata, gene_symbols): try: mapped_gene_symbol = get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id) - except: - return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be mapped to the dataset organism."} - if mapped_gene_symbol: - gene_symbols = (mapped_gene_symbol,) - if not check_gene_in_dataset(adata, gene_symbols): - return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be found in the h5ad file."} - else: + # Last chance - See if a normalized gene symbol is present in the dataset + if not mapped_gene_symbol: + dataset_genes = adata.var['gene_symbol'].unique().tolist() + mapped_gene_symbol = normalize_searched_gene(dataset_genes, gene_symbol) + if not mapped_gene_symbol: + raise Exception("Could not map gene symbol to dataset organism.") + + except Exception as e: + print(str(e), file=sys.stderr) return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be mapped to the dataset organism."} + gene_symbols = (mapped_gene_symbol,) + if not check_gene_in_dataset(adata, gene_symbols): + return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be found in the h5ad file."} + # Filter genes and slice the adata to get a dataframe # with expression and its observation metadata try: diff --git a/www/api/resources/svg_data.py b/www/api/resources/svg_data.py index cc0a8be2..f70db6ef 100644 --- a/www/api/resources/svg_data.py +++ b/www/api/resources/svg_data.py @@ -22,6 +22,13 @@ def __init__(self, message="") -> None: self.message = message super().__init__(self.message) +def normalize_searched_gene(gene_list, chosen_gene): + """Convert to case-insensitive version of gene. Returns None if gene not found in dataset.""" + for g in gene_list: + if chosen_gene.lower() == str(g).lower(): + return g + return None + def get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id): """ Maps a gene symbol to its corresponding orthologous gene symbol in a given dataset. @@ -140,15 +147,20 @@ def get(self, dataset_id): if not check_gene_in_dataset(adata, gene_symbols): try: mapped_gene_symbol = get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id) + + # Last chance - See if a normalized gene symbol is present in the dataset + if not mapped_gene_symbol: + dataset_genes = adata.var['gene_symbol'].unique().tolist() + mapped_gene_symbol = normalize_searched_gene(dataset_genes, gene_symbol) + if not mapped_gene_symbol: + raise Exception("Could not map gene symbol to dataset organism.") + except: return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be mapped to the dataset organism."} - if mapped_gene_symbol: - gene_symbols = (mapped_gene_symbol,) - if not check_gene_in_dataset(adata, gene_symbols): - return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be found in the h5ad file."} - else: - return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be mapped to the dataset organism."} + gene_symbols = (mapped_gene_symbol,) + if not check_gene_in_dataset(adata, gene_symbols): + return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be found in the h5ad file."} try: gene_filter = adata.var.gene_symbol.isin(gene_symbols) diff --git a/www/api/resources/tsne_data.py b/www/api/resources/tsne_data.py index 2909a4e6..6c77f3a9 100644 --- a/www/api/resources/tsne_data.py +++ b/www/api/resources/tsne_data.py @@ -46,6 +46,13 @@ def __init__(self, message="") -> None: self.message = message super().__init__(self.message) +def normalize_searched_gene(gene_list, chosen_gene): + """Convert to case-insensitive version of gene. Returns None if gene not found in dataset.""" + for g in gene_list: + if chosen_gene.lower() == str(g).lower(): + return g + return None + def get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id): """ Maps a gene symbol to its corresponding orthologous gene symbol in a given dataset. @@ -315,15 +322,20 @@ def post(self, dataset_id): if not check_gene_in_dataset(adata, gene_symbols): try: mapped_gene_symbol = get_mapped_gene_symbol(gene_symbol, gene_organism_id, dataset_organism_id) + + # Last chance - See if a normalized gene symbol is present in the dataset + if not mapped_gene_symbol: + dataset_genes = adata.var['gene_symbol'].unique().tolist() + mapped_gene_symbol = normalize_searched_gene(dataset_genes, gene_symbol) + if not mapped_gene_symbol: + raise Exception("Could not map gene symbol to dataset organism.") + except: return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be mapped to the dataset organism."} - if mapped_gene_symbol: - gene_symbols = (mapped_gene_symbol,) - if not check_gene_in_dataset(adata, gene_symbols): - return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be found in the h5ad file."} - else: - return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be mapped to the dataset organism."} + gene_symbols = (mapped_gene_symbol,) + if not check_gene_in_dataset(adata, gene_symbols): + return {"success": -1, "message": f"The searched gene symbol {gene_symbol} could not be found in the h5ad file."} gene_filter = adata.var.gene_symbol.isin(gene_symbols) diff --git a/www/js/classes/tilegrid.js b/www/js/classes/tilegrid.js index 849b615d..0d8bb65a 100644 --- a/www/js/classes/tilegrid.js +++ b/www/js/classes/tilegrid.js @@ -188,15 +188,12 @@ class TileGrid { geneSymbolInput = Array.isArray(geneSymbols) ? geneSymbols[0] : geneSymbols; } - - if (!isMultigene) { - await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbolInput))); - } else { - // Sometimes multigene fails to render due to OOM errors, so we want to try each tile individually - for (const tile of this.tiles) { - await tile.renderDisplay(geneSymbolInput); - } + // Sometimes fails to render due to OOM errors, so we want to try each tile individually + for (const tile of this.tiles) { + await tile.renderDisplay(geneSymbolInput); } + + // await Promise.allSettled(this.tiles.map( async tile => await tile.renderDisplay(geneSymbolInput))); } }; From 068617af66748e592765f292fd6c087e2a610262 Mon Sep 17 00:00:00 2001 From: adkinsrs Date: Fri, 19 Jan 2024 16:58:38 -0500 Subject: [PATCH 8/8] orthology works. SVG tissue scoping works. SVG legend and tooltips does not work --- lib/gear/orthology.py | 15 ++ www/expression.html | 8 +- www/js/classes/tilegrid.js | 356 ++++++++++++++++++++++++++++++------- www/js/expression.js | 22 ++- 4 files changed, 333 insertions(+), 68 deletions(-) diff --git a/lib/gear/orthology.py b/lib/gear/orthology.py index 5559e7d4..60b99edf 100644 --- a/lib/gear/orthology.py +++ b/lib/gear/orthology.py @@ -155,6 +155,14 @@ def map_single_gene(gene_symbol:str, orthomap_file: Path): """ # Read HDF5 file using Pandas read_hdf gene_symbol_dict = create_orthology_gene_symbol_dict(orthomap_file) + + # Check if case-insensitive gene symbol is in dictionary + gene_symbol = gene_symbol.lower() + for key in gene_symbol_dict.keys(): + if gene_symbol == key.lower(): + gene_symbol = key + break + # NOTE: Not all genes can be mapped. Unmappable genes do not change in the original dataframe. return gene_symbol_dict.get(gene_symbol, None) @@ -172,5 +180,12 @@ def map_multiple_genes(gene_symbols:list, orthomap_file: Path): # Read HDF5 file using Pandas read_hdf gene_symbol_dict = create_orthology_gene_symbol_dict(orthomap_file) + # Check if case-insensitive gene symbols are in dictionary + gene_symbols = [gene_symbol.lower() for gene_symbol in gene_symbols] + for key in gene_symbol_dict.keys(): + if key.lower() in gene_symbols: + gene_symbols[gene_symbols.index(key.lower())] = key + + # NOTE: Not all genes can be mapped. Unmappable genes do not change in the original dataframe. return { gene_symbol: gene_symbol_dict[gene_symbol] for gene_symbol in gene_symbols if gene_symbol in gene_symbol_dict} \ No newline at end of file diff --git a/www/expression.html b/www/expression.html index a6297599..fc87aca7 100644 --- a/www/expression.html +++ b/www/expression.html @@ -185,10 +185,10 @@

    miRNA tree

    Scoring method:
    - + + +
    diff --git a/www/js/classes/tilegrid.js b/www/js/classes/tilegrid.js index 0d8bb65a..5ecbe1e3 100644 --- a/www/js/classes/tilegrid.js +++ b/www/js/classes/tilegrid.js @@ -8,6 +8,8 @@ For the given layout, a single-gene grid and a multi-gene grid are generated. const plotlyPlots = ["bar", "line", "scatter", "tsne/umap_dynamic", "violin"]; // "tsne_dynamic" is a legacy option const scanpyPlots = ["pca_static", "tsne_static", "umap_static"]; // "tsne" is a legacy option +// Epiviz overrides the