diff --git a/tests/test/gene_collection_manager.test.js b/tests/test/gene_collection_manager.test.js index 0e25c355..a6d4eb94 100644 --- a/tests/test/gene_collection_manager.test.js +++ b/tests/test/gene_collection_manager.test.js @@ -129,6 +129,16 @@ const mockSaveNewGeneCollection = async (page) => { }) } +const mockSaveGeneCollectionChanges = async (page) => { + await page.route(`${gearBase}/cgi/save_genecart_changes.cgi`, async route => { + const json = { + "gene_cart": "{\"id\": 334, \"user_id\": 1, \"gctype\": \"unweighted-list\", \"label\": \"updated test\", \"organism_id\": 1, \"ldesc\": \"This is the unweighted description\", \"share_id\": \"06c1cdd3\", \"is_public\": 1, \"is_domain\": null, \"date_added\": \"2023-11-01 17:10:36\", \"genes\": [\"0610040J01Rik\", \"Zkscan1\", \"1500011K16Rik\", \"1700025G04Rik\", \"Zfp60\", \"1810037I17Rik\", \"2010111I01Rik\", \"2300009A05Rik\", \"Zfp24\", \"Zdhhc2\", \"4931406P16Rik\", \"Ypel2\", \"5031439G07Rik\", \"Wwp1\", \"Wsb2\", \"Wls\", \"Wasf1\", \"Vwc2\", \"Vwa1\", \"Aatk\", \"Vps36\", \"Vps37b\", \"Abca8a\", \"Vldlr\", \"Abhd4\", \"Vcl\", \"Utrn\", \"Acsbg1\", \"Usp1\", \"Adam10\", \"Adam17\", \"Adam23\", \"Adamts17\", \"Adamts5\", \"Ung\", \"Unc119\", \"Adgrg6\", \"Ucp2\", \"Ado\", \"Ado\", \"Uchl1\", \"Adra2c\", \"Afap1l2\", \"Ubl3\", \"Ube2e2\", \"Ube2c\", \"Ahr\", \"Ak3\", \"Ak6\", \"Tyms\", \"Akr1b8\", \"Twf1\", \"Alcam\", \"Alad\", \"Tubg1\", \"Ttyh1\", \"Ttyh2\", \"Tst\", \"Tspo\", \"Angpt2\", \"Ank3\", \"Tspan18\", \"Tspan13\", \"Tspan15\", \"Tspan17\", \"Tsc22d4\", \"Anxa2\", \"Trim2\", \"Trim13\", \"Arfip1\", \"Trappc2\", \"Arhgap19\", \"Arhgap24\", \"Arhgap32\", \"Arhgap39\", \"Arhgef10\", \"Arhgef28\", \"Tpst1\", \"Tppp3\", \"Tpp1\", \"Arid5b\", \"Tpd52\", \"Tox\", \"Arl8a\", \"Arnt2\", \"Top2a\", \"Arrdc3\", \"Art3\", \"Arvcf\", \"Arvcf\", \"Arxes2\", \"Tns3\", \"Asb8\", \"Aspa\", \"Asrgl1\", \"Atad2\", \"Asxl3\", \"Tnik\", \"Atad2\", \"Tnfrsf21\", \"Atg3\", \"Tnfaip6\", \"Tmpo\", \"Atp1b1\", \"Tmod2\", \"Tmem9b\", \"Tmem63b\", \"Atp8a1\", \"Aurkb\", \"Tmem245\", \"B4galt6\", \"Bach1\", \"Bambi\", \"Tmem117\", \"Tm7sf3\", \"Bcas1\", \"Tk1\", \"Tjp1\", \"Bcl7a\", \"Thtpa\", \"Bicd1\", \"Bin3\", \"Birc5\", \"Tgfbr3\", \"Borcs5\", \"Bpnt1\", \"Tex30\", \"Tdrkh\", \"Tead1\", \"Bzw2\", \"Tcp11l1\", \"Tcf19\", \"Tbx2\", \"Cab39l\", \"Cadm1\", \"Cadm4\", \"Camk2d\", \"Tbc1d10a\", \"Tax1bp3\", \"Capg\", \"Tanc2\", \"Capn5\", \"Taldo1\", \"Tagln2\", \"Taf13\", \"Tacc1\", \"Syt11\", \"Syngr1\", \"Cbx6\", \"Svip\", \"Ccdc13\", \"Ccdc28b\", \"Stx7\", \"Strbp\", \"Ccna2\", \"Ccnd1\", \"Ccnb2\", \"Stmn2\", \"Ccser2\"], \"folder_id\": null, \"folder_parent_id\": null, \"folder_label\": null, \"user_name\": \"Test Armstrong\", \"gene_count\": 159, \"organism\": \"Mus musculus\", \"is_owner\": false}", + "success": 1 + }; + await route.fulfill({ json }); + }); +} + /** * Mocks the delete gene collection functionality. * @param {Page} page - The page object. @@ -281,7 +291,6 @@ const mockDownloadUnweightedGeneCollectionMembers = async (page) => { describe('Gene Collection Manager', function () { this.retries(3); - this.timeout(10000); // default is 2000 let browserIndex = 0; @@ -498,7 +507,7 @@ describe('Gene Collection Manager', function () { // Selecting should deselect "All" await groupAffiliatedOption.click(); - // ! This class seems to not be detected + // TODO: detecting the class seems to be flaky when all tests are run await expect(groupAffiliatedOption).toHaveClass(/js-selected/); await expect(yourCollectionsOption).toHaveClass(/js-selected/); await expect(allFacet).not.toHaveClass(/js-selected/); @@ -628,6 +637,9 @@ describe('Gene Collection Manager', function () { it("should update gene collection when save button is clicked", async () => { await page.locator("css=#result_gc_id_334 .js-edit-gc").click(); await page.locator("css=#result_gc_id_334_editable_title").fill("updated test"); + + await mockSaveGeneCollectionChanges(page); + await page.locator("css=#result_gc_id_334 .js-edit-gc-save").click(); await expect(page.locator("css=#result_gc_id_334_editable_title")).not.toBeVisible(); // Check that the search results were updated diff --git a/tests/test/multigene_curator.test.js b/tests/test/multigene_curator.test.js index 38308ced..7922eac0 100644 --- a/tests/test/multigene_curator.test.js +++ b/tests/test/multigene_curator.test.js @@ -1,7 +1,7 @@ #!/usr/bin/env mocha /* - Unit tests for dataset_curator.js + Unit tests for multigene_curator.js */ // Some setup help by https://publishing-project.rivendellweb.net/testing-front-end-with-mocha-and-playwright-2/ diff --git a/www/compare_datasets.html b/www/compare_datasets.html index efb12b72..91f90e20 100644 --- a/www/compare_datasets.html +++ b/www/compare_datasets.html @@ -35,7 +35,7 @@ - + @@ -493,7 +493,7 @@
Loading ...
"); - const obs_data = await fetch_h5ad_observations(dataset_id); - const cat_obs = obs_data.obs_levels; // cat->groups - const all_obs = obs_data.obs_columns; // Array - const noncat_obs = Object.values(all_obs).filter(x => Object.keys(cat_obs).indexOf(x) === -1); // Array - - // If dataset has no observations, indicate it does not and hide condition controls - if (! Object.keys(cat_obs).length) { - $('#condition_tabs').hide(); - $("#conditions_accordion").removeClass("alert-info").addClass("alert-danger"); - $("#conditions_accordion").text("There are no categorical observations found for this dataset."); - return; - } +const fetchDatasets = async () => { + try { + return await apiCallsMixin.fetchDatasets(); + } catch (error) { + logErrorInConsole(error); + const msg = "Could not fetch datasets. Please contact the gEAR team." + createToast(msg); + throw new Error(msg); + } +} - $('#condition_tabs').show(); +/* Fetch gene collection members */ +const fetchGeneCartMembers = async (geneCartId) => { + try { + const {gene_symbols, success} = await apiCallsMixin.fetchGeneCartMembers(geneCartId); + if (!success) { + throw new Error("Could not fetch gene collection members. You can still enter genes manually."); + } + return gene_symbols; + } catch (error) { + logErrorInConsole(error); + const msg = "Could not fetch gene collection members. You can still enter genes manually."; + createToast(msg); + throw new Error(msg); + } +} - // Render templates - const selector_tmpl = $.templates("#dataset_condition_options"); - const selector_html = selector_tmpl.render(cat_obs); - $("#conditions_accordion").html(selector_html); +/* Fetch gene collections */ +const fetchGeneCarts = async () => { + const cartType = "unweighted-list"; + try { + return await apiCallsMixin.fetchGeneCarts(cartType); + } catch (error) { + logErrorInConsole(error); + const msg = "Could not fetch gene collections. You can still enter genes manually."; + createToast(msg); + throw new Error(msg); + } +} - if (noncat_obs.length) { - const noncat_tmpl = $.templates("#non_categories_list"); - const noncat_html = noncat_tmpl.render({noncat_obs}); - $("#noncats").html(noncat_html); +const fetchGeneSymbols = async (datasetId, analysisId) => { + try { + const data = await apiCallsMixin.fetchGeneSymbols(datasetId, analysisId); + return [...new Set(data.gene_symbols)]; // Dataset may have a gene repeated in it, so resolve this. + } catch (error) { + logErrorInConsole(error); + const msg = "Could not fetch gene symbols for this dataset. Please contact the gEAR team." + createToast(msg); + return []; } +} - // Since we want the default state of the category groups to be unchecked, - // we do not set an initial state of stored checked conditions - if (obs_data.has_replicates == 1) { - $("#statistical_test_label").html(""); - $("#statistical_test").attr("disabled", false); - } else { - $("#statistical_test_label").html( - "Not applicable since this dataset has no replicates" - ); - $("#statistical_test").attr("disabled", true); +const geneSelectUpdate = async (analysisId=null) => { + // Populate gene select element + try { + const geneSymbols = await fetchGeneSymbols(datasetId, analysisId); + updateGeneOptions(geneSymbols); // Come from curator specific code + } catch (error) { + logErrorInConsole(error); } } -$('#condition_x_tab').on('shown.bs.tab', (e) => { +const getComparisons = async (event) => { - condition_y = update_selected_conditions(); + // set loading icon + event.target.classList.add("is-loading"); - // Load condition_x stuff - $('.js-cat-check').prop("checked", false); - $('.js-group-check').prop("checked", false); - for (const cat in condition_x) { - for (const elem of condition_x[cat]) { - $(`input[data-group="${cat};-;${elem}"]`).prop("checked", true); - } - $('.js-group-check').change(); // trigger so the cat checkbox matches up - } + const filters = JSON.stringify(facetWidget.filters); -}) + const compareSeries = document.getElementById("compare_series").value -$('#condition_y_tab').on('shown.bs.tab', (e) => { + // Get all checked x and y series + const checkedX = JSON.stringify([...document.querySelectorAll("#compare_x input:checked")].map((elt) => elt.value)); + const checkedY = JSON.stringify([...document.querySelectorAll("#compare_y input:checked")].map((elt) => elt.value)); - condition_x = update_selected_conditions(); + const foldChangeCutoff = document.getElementById("fc_cutoff").value; + const stdDevNumCutoff = document.getElementById("standard_deviation").value; + const logTransformation = document.getElementById("log_base").value; + const statisticalTest = document.getElementById("statistical_test").value; - // Load condition_y stuff - $('.js-cat-check').prop("checked", false); - $('.js-group-check').prop("checked", false); - for (const cat in condition_y) { - for (const elem of condition_y[cat]) { - $(`input[data-group="${cat};-;${elem}"]`).prop("checked", true); + try { + const data = await fetchDatasetComparison(datasetId, filters, compareSeries, checkedX, checkedY, foldChangeCutoff, stdDevNumCutoff, logTransformation, statisticalTest); + if (data?.success < 1) { + throw new Error(data?.message || "Could not fetch dataset comparison. Please contact the gEAR team."); } - $('.js-group-check').change(); // trigger so the cat checkbox matches up + compareData = data; + plotDataToGraph(compareData); + + // If any genes selected, update plot annotations (since plot was previously purged) + const sortedGenes = geneSelect.selectedOptions.map((opt) => opt.data.value).sort(); + updatePlotAnnotations(sortedGenes); + + // Show button to add genes to gene cart + document.getElementById("gene_cart_btn_c").classList.remove("is-hidden"); + + // Hide this view + document.getElementById("content_c").classList.add("is-hidden"); + // Generate and display "post-plotting" view/container + document.getElementById("post_plot_content_c").classList.remove("is-hidden"); + + } catch (error) { + console.error(error); + handleGetComparisonError(datasetId, checkedX, checkedY); + } finally { + event.target.classList.remove("is-loading"); } -}) - -$(document).on('change', '.js-cat-check', function (e) { - // If turned on, check all group boxes - // If turned off, uncheck all group boxes - const checked = $(this).prop("checked"); - - const id = this.id; - const category = id.replace('_check', ''); - const escapedCategory = $.escapeSelector(category); - const category_collaspable = $(`#${escapedCategory}_body`); - category_collaspable.find('input[type="checkbox"]').prop({checked}); + // When a plot configuration ID is selected, populate the plot configuration post textbox + const plotConfigElts = ["statistical_test", "pval_cutoff", "cutoff_filter_action", "log_base", "fc_cutoff", "standard_deviation"]; + for (const elt of plotConfigElts) { + // if value is empty, set to "None", or if disabled, set to "N/A" + let value = document.getElementById(elt).disabled ? "N/A" : document.getElementById(elt).value || "None" - // Expand collaspable since category was focused on - category_collaspable.collapse('show'); - - update_axis_labels(); -}) - -$(document).on('click', '.js-cat-collapse', function (e) { - // If category was clicked, then toggle collapsable element - // Controlling via JS instead of "data-target" since we may need to escape CSS selectors - const { id } = this; - const category = id.replace('_collapse', ''); - const escapedCategory = $.escapeSelector(category); - const category_collaspable = $(`#${escapedCategory}_body`); - category_collaspable.collapse('toggle'); -}) + // Append extra flavor text + if (elt == "log_base" && !(value === "raw" )) { + value = `log${value}` + } -$(document).on('change', '.js-group-check', function(e) { - // https://css-tricks.com/indeterminate-checkboxes/ - // After changing checkbox status, check siblings - // and determine if category checkbox should be - // checked, not checked, or indeterminate - - const checked = $(this).prop("checked"); - - // Get category name out of the checkbox ID - const id = $(this).data("group"); - const category = id.split(';-;')[0] - const escapedCategory = $.escapeSelector(category); - const category_header = $(`#${escapedCategory}_check`); - const category_collaspable = $(`#${escapedCategory}_body`); - - // Get checked status of all other checkboxes in this category - // If there is a combination of checked/unchecked the "each" loop breaks early - let all = true; - $(category_collaspable).find('input[type="checkbox"]').each(function(){ - return all = ($(this).prop("checked") === checked); - }); + if (elt == "standard_deviation" && !(value === "0" )) { + value = `±${value}` + } - if (all) { - // All group checkboxes are the same as the category checkbox - category_header.prop({ - "indeterminate": false, - "checked": checked - }); - } else { - // All group checkbox states are mixed. Category checkbox is indeterminate and unchecked - category_header.prop({ - "indeterminate": true, - "checked": false - }); + document.getElementById(`${elt}_post`).textContent = value; } - update_axis_labels(); -}) - -function update_axis_labels() { - // Update the "axis label" input boxes - if ($("#condition_x_tab").hasClass("active")) { - const curr_condition_x = update_selected_conditions(); - $('#x_label').val(stringify_all_conditions(curr_condition_x)); - } else { - // on #condition_y_tab - const curr_condition_y = update_selected_conditions(); - $('#y_label').val(stringify_all_conditions(curr_condition_y)); - } } -// Get all chosen condition groups and return as a semicolon-joined string -function stringify_all_conditions(condition) { - const all_conditions = [] - for (const property in condition) { - // If no groups for an observation are selected, delete filter - if (condition[property].length) { - all_conditions.push(...condition[property]) - - } - } - return all_conditions.join(";"); - } - - function sanitize_condition(condition) { - const sanitized_condition = {} - for (const property in condition) { - // If no groups for an observation are selected, delete filter - if (condition[property].length) { - sanitized_condition[property] = condition[property]; - } - } - return sanitized_condition; +const getSeriesItems = (series) => { + return facetWidget.aggregations.find((agg) => agg.name === series).items; } -$(document).on("build_jstrees", () => populate_dataset_selection_controls()); +const getSeriesNames = (seriesItems) => { + return seriesItems.map((item) => item.name); +} -async function populate_dataset_selection_controls() { - const dataset_id = getUrlParameter("dataset_id"); - $('#pre_dataset_spinner').show(); - await $.ajax({ - type: "POST", - url: "./cgi/get_h5ad_dataset_list.cgi", - data: { - session_id: CURRENT_USER.session_id, - for_page: "compare_dataset", - include_dataset_id: dataset_id, - }, - dataType: "json", - success(data) { - let counter = 0 - // Populate select box with dataset information owned by the user - const user_datasets = []; - if (data.user.datasets.length > 0) { - // User has some profiles - $.each(data.user.datasets, (_i, item) => { - if (item) { - user_datasets.push({ value: counter++, text: item.title, dataset_id : item.id, organism_id: item.organism_id }); - } - }); - } - // Next, add datasets shared with the user - const shared_datasets = []; - if (data.shared_with_user.datasets.length > 0) { - // User has some profiles - $.each(data.shared_with_user.datasets, (_i, item) => { - if (item) { - shared_datasets.push({ value: counter++, text: item.title, dataset_id : item.id, organism_id: item.organism_id }); - } - }); - } - // Now, add public datasets - const domain_datasets = []; - if (data.public.datasets.length > 0) { - // User has some profiles - $.each(data.public.datasets, (_i, item) => { - if (item) { - domain_datasets.push({ value: counter++, text: item.title, dataset_id : item.id, organism_id: item.organism_id }); - } - }); - } +const handleGetComparisonError = (datasetID, conditionX, conditionY) => { + const msg = `Could not fetch dataset comparison. Please contact the gEAR team.`; + createToast(msg); + console.error(msg); +} - dataset_tree.userDatasets = user_datasets; - dataset_tree.sharedDatasets = shared_datasets; - dataset_tree.domainDatasets = domain_datasets; - dataset_tree.generateTree(); - - // was there a requested dataset ID already? - if (dataset_id !== undefined) { - $("#dataset_id").val(dataset_id); - try { - // Had difficulties triggering a "select_node.jstree" event, so just add the data info here - const tree_leaf = dataset_tree.treeData.find(e => e.dataset_id === dataset_id); - $('#dataset_id').text(tree_leaf.text); - $('#dataset_id').data("organism-id", tree_leaf.organism_id); - $('#dataset_id').data("dataset-id", tree_leaf.dataset_id); - $("#dataset_id").trigger("change"); - } catch { - console.error(`Dataset id ${dataset_id} was not returned as a public/private/shared dataset`); - } - } - }, - error(xhr, status, msg) { - report_error(`Failed to load dataset list because msg: ${msg}`); - }, - }); - $('#pre_dataset_spinner').hide(); +/* Transform and load dataset data into a "tree" format */ +const loadDatasetTree = async () => { + const userDatasets = []; + const sharedDatasets = []; + const domainDatasets = []; + try { + const datasetData = await fetchDatasets(); + + let counter = 0; + + // Populate select box with dataset information owned by the user + if (datasetData.user.datasets.length > 0) { + // User has some profiles + for (const item of datasetData.user.datasets) { + if (item) { + userDatasets.push({ value: counter++, text: item.title, dataset_id : item.id, organism_id: item.organism_id }); + } + }; + } + // Next, add datasets shared with the user + if (datasetData.shared_with_user.datasets.length > 0) { + for (const item of datasetData.shared_with_user.datasets) { + if (item) { + sharedDatasets.push({ value: counter++, text: item.title, dataset_id : item.id, organism_id: item.organism_id }); + } + }; + } + // Now, add public datasets + if (datasetData.public.datasets.length > 0) { + for (const item of datasetData.public.datasets) { + if (item) { + domainDatasets.push({ value: counter++, text: item.title, dataset_id : item.id, organism_id: item.organism_id }); + } + }; + } + datasetTree.userDatasets = userDatasets; + datasetTree.sharedDatasets = sharedDatasets; + datasetTree.domainDatasets = domainDatasets; + datasetTree.generateTree(); + } catch (error) { + document.getElementById("dataset_s_failed").classList.remove("is-hidden"); + } } -function plot_data_to_graph(data) { - $("#selection_methods_c").show(); +/* Transform and load gene collection data into a "tree" format */ +const loadGeneCarts = async () => { + try { + const geneCartData = await fetchGeneCarts(); + const carts = {}; + const cartTypes = ['domain', 'user', 'group', 'shared', 'public']; + let cartsFound = false; + + // Loop through the different types of gene collections and add them to the carts object + for (const ctype of cartTypes) { + carts[ctype] = []; + + if (geneCartData[`${ctype}_carts`].length > 0) { + cartsFound = true; + + for (const item of geneCartData[`${ctype}_carts`]) { + carts[ctype].push({value: item.id, text: item.label }); + }; + } + } + + geneCartTree.domainGeneCarts = carts.domain; + geneCartTree.userGeneCarts = carts.user; + geneCartTree.groupGeneCarts = carts.group; + geneCartTree.sharedGeneCarts = carts.shared; + geneCartTree.publicGeneCarts = carts.public; + geneCartTree.generateTree(); + /*if (!cartsFound ) { + // ? Put some warning if carts not found + $('#gene_cart_container').show(); + }*/ + + } catch (error) { + document.getElementById("gene_s_failed").classList.remove("is-hidden"); + } +} - const point_labels = []; - let perform_ranking = false; +const plotDataToGraph = (data) => { - if ($("#statistical_test").val()) { - perform_ranking = true; - } + const statisticalTest = document.getElementById("statistical_test").value; - let plotdata = null; + const pointLabels = []; + const performRanking = statisticalTest ? true : false; - if (!perform_ranking) { - for (i = 0; i < data.symbols.length; i++) { - point_labels.push(`Gene symbol: ${data.symbols[i]}`); - } + const plotData = []; - plotdata = [ - { - id: data.symbols, - pvals: data.pvals_adj, - x: data.x, - y: data.y, - foldchange: data.fold_changes, - mode: "markers", - type: "scatter", - text: point_labels, - marker: { - color: "#2F103E", - size: 4, - }, - }, - ]; - } else { - const pval_cutoff = parseFloat($("#test_pval_cutoff").val()); + if (performRanking) { + const pValCutoff = document.getElementById("pval_cutoff").value; + const pvalCutoff = parseFloat(pValCutoff); const passing = { x: [], y: [], labels: [], id: [], pvals: [], foldchange: []}; const failing = { x: [], y: [], labels: [], id: [], pvals: [], foldchange: []}; - for (i = 0; i < data.x.length; i++) { - // pvals_adj array consist of 1-element arrays, so let's flatten to prevent potential issues - // Caused by rank_genes_groups output (1 inner array per query comparison group) - data.pvals_adj = data.pvals_adj.flat(); + data.x.forEach((trace, i) => { + // pvals_adj array consist of 1-element arrays, so let's flatten to prevent potential issues + // Caused by rank_genes_groups output (1 inner array per query comparison group) + data.pvals_adj = data.pvals_adj.flat(); + + const thisPval = parseFloat(data.pvals_adj[i]); - const this_pval = parseFloat(data.pvals_adj[i]); + const arrayToPushInto = (thisPval <= pvalCutoff) ? passing : failing; - if ((this_pval <= pval_cutoff)) { - // good scoring match - passing.x.push(data.x[i]); - passing.y.push(data.y[i]); - passing.foldchange.push(data.fold_changes[i]); - passing.labels.push( + arrayToPushInto.x.push(trace); + arrayToPushInto.y.push(data.y[i]); + arrayToPushInto.foldchange.push(data.fold_changes[i]); + arrayToPushInto.labels.push( "Gene symbol: " + data.symbols[i] + " P-value: " + - this_pval.toPrecision(6) + thisPval.toPrecision(6) ); - passing.id.push(data.symbols[i]); - passing.pvals.push(data.pvals_adj[i]); + arrayToPushInto.id.push(data.symbols[i]); + arrayToPushInto.pvals.push(data.pvals_adj[i]); + + }); + + const passColor = CURRENT_USER.colorblind_mode ? 'rgb(0, 34, 78)' : "#FF0000"; + const failColor = CURRENT_USER.colorblind_mode ? 'rgb(254, 232, 56)' : "#A1A1A1"; + + const statAction = document.getElementById("cutoff_filter_action").value; + if (statAction === "colorize") { + const passingObj = { + id: passing.id, + pvals: passing.pvals, + x: passing.x, + y: passing.y, + foldchange: passing.foldchange, + mode: "markers", + name: "Passed cutoff", + type: "scatter", + text: passing.labels, + marker: { + color: new Array(passing.x.length).fill(passColor, 0, passing.x.length), + size: 4, + }, + } + // store original marker color as a deep copy + passingObj.marker.origColor = JSON.parse(JSON.stringify(passingObj.marker.color)); + + const failingObj = { + id: failing.id, + pvals: failing.pvals, + x: failing.x, + y: failing.y, + foldchange: failing.foldchange, + mode: "markers", + name: "Did not pass cutoff", + type: "scatter", + text: failing.labels, + marker: { + color: new Array(failing.x.length).fill(failColor, 0, failing.x.length), + size: 4, + }, + } + // store original marker color as a deep copy + failingObj.marker.origColor = JSON.parse(JSON.stringify(failingObj.marker.color)); + + plotData.push(passingObj); + plotData.push(failingObj); } else { - // this one didn't pass the p-value cutoff - failing.x.push(data.x[i]); - failing.y.push(data.y[i]); - failing.foldchange.push(data.fold_changes[i]); - failing.labels.push( - "Gene symbol: " + - data.symbols[i] + - " P-value: " + - this_pval.toPrecision(6) - ); - failing.id.push(data.symbols[i]); - failing.pvals.push(data.pvals_adj[i]); - } + + const passingObj = { + id: passing.id, + pvals: passing.pvals, + x: passing.x, + y: passing.y, + foldchange: passing.foldchange, + mode: "markers", + type: "scatter", + text: passing.labels, + marker: { + color: new Array(passing.x.length).fill("#000000" , 0, passing.x.length), + size: 4, + }, + } + // store original marker color as a deep copy + passingObj.marker.origColor = JSON.parse(JSON.stringify(passingObj.marker.color)); + + plotData.push(passingObj); } - pass_color = CURRENT_USER.colorblind_mode ? 'rgb(0, 34, 78)' : "#FF0000"; - fail_color = CURRENT_USER.colorblind_mode ? 'rgb(254, 232, 56)' : "#A1A1A1"; + } else { + for (const gene of data.symbols) { + pointLabels.push(`Gene symbol: ${gene}`); + } - plotdata = $("input[name='stat_action']:checked").val() == "colorize" ? [ - { - id: passing.id, - pvals: passing.pvals, - x: passing.x, - y: passing.y, - foldchange: passing.foldchange, - mode: "markers", - name: "Passed cutoff", - type: "scatter", - text: passing.labels, - marker: { - color: pass_color, - size: 4, - }, - }, - { - id: failing.id, - pvals: failing.pvals, - x: failing.x, - y: failing.y, - foldchange: failing.foldchange, - mode: "markers", - name: "Did not pass cutoff", - type: "scatter", - text: failing.labels, - marker: { - color: fail_color, - size: 4, - }, - }, - ] : [ - { - id: passing.id, - pvals: passing.pvals, - x: passing.x, - y: passing.y, - foldchange: passing.foldchange, + const dataObj = { + id: data.symbols, + pvals: data.pvals_adj, + x: data.x, + y: data.y, + foldchange: data.fold_changes, mode: "markers", type: "scatter", - text: passing.labels, + text: pointLabels, marker: { - color: "#2F103E", - size: 4, + color: new Array(data.x.length).fill("#000000", 0, data.x.length), + size: 4, }, - }, - ]; + } + // store original marker color as a deep copy + dataObj.marker.origColor = JSON.parse(JSON.stringify(dataObj.marker.color)); + + plotData.push(dataObj); } const layout = { - title: $("#dataset_id").text(), + title: titleText || "Dataset Comparison", xaxis: { - title: $('#x_label').val().length ? $('#x_label').val() : JSON.stringify(data.condition_x_idx), - type: "", + title: xaxisText || data.condition_x.join(", "), }, yaxis: { - title: $('#y_label').val().length ? $('#y_label').val() : JSON.stringify(data.condition_y_idx), - type: "", + title: yaxisText || data.condition_y.join(", "), }, annotations: [], - margin: { t: 40 }, hovermode: "closest", dragmode: "select", - modebar: {orientation: "v"} }; - annotation_color = CURRENT_USER.colorblind_mode ? 'rgb(125, 124, 118)' : "crimson"; - - // Take genes to search for and highlight their datapoint in the plot - const genes_not_found = []; - if ($('#highlighted_genes').val()) { - const searched_genes = $('#highlighted_genes').val().replace(/,?\s/g, ",").split(","); - searched_genes.forEach((gene) => { - let found = false; - plots: - for (i = 0; i < plotdata.length; i++) { - genes: - for (j = 0; j < plotdata[i].id.length; j++) { - if (gene.toLowerCase() === plotdata[i].id[j].toLowerCase() ) { - // If gene is found add an annotation arrow - layout.annotations.push({ - xref: "x", - yref: "y", - x: plotdata[i].x[j], - y: plotdata[i].y[j], - text:plotdata[i].id[j], - font: { - color: annotation_color, - }, - showarrow: true, - arrowcolor: annotation_color, - }); - found = true; - break plots; - } - } - } - if (! found) - genes_not_found.push(gene); - }); + const config = { + editable: true, // allows user to edit plot title, axis labels, and legend + showLink: false } + const plotContainer = document.getElementById("plot_container"); + plotContainer.replaceChildren(); // erase plot - $("#plot_loading").hide(); - const graphDiv = document.getElementById("myChart"); - Plotly.newPlot(graphDiv, plotdata, layout, { showLink: false }); - $("#selected_label").hide(); - $("#controls_label").show(); - $('#weighted_gene_cart_c').show(); + // NOTE: Plot initially is created to a default width but is responsive. + // Noticed container within our "column" will make full-width go beyond the screen - // If searched-for genes were not found, display under plot - if (genes_not_found.length) { - const genes_not_found_str = genes_not_found.join(", "); - $("#genes_not_found").text(`Searched genes not found: ${genes_not_found_str}`); - $("#genes_not_found").show(); - } else { - $("#genes_not_found").hide(); - } + const plotlyPreview = document.createElement("div"); + plotlyPreview.id = "plotly_preview"; + plotlyPreview.classList.add("container", "is-max-desktop"); + plotContainer.append(plotlyPreview); - // If plot data is selected, create the right-column table and do other misc things - graphDiv.on("plotly_selected", (eventData) => { - selected_data = eventData; - selected_gene_data = []; - - eventData.points.forEach((pt) => { - // Some warnings on using toFixed() here: https://stackoverflow.com/a/12698296/1368079 - // Each trace has its own "pointNumber" ids so gene symbols and pvalues needed to be passed in for each plotdata trace - selected_gene_data.push({ - gene_symbol: pt.data.id[pt.pointNumber], - pvals: $("#statistical_test").val() ? pt.data.pvals[pt.pointNumber].toExponential(2) : "NA", - foldchange: pt.data.foldchange[pt.pointNumber].toFixed(1), - }); - }); + Plotly.purge("plotly_preview"); // clear old Plotly plots - // Sort by adjusted p-value in descending order either by fold change or p-values - selected_gene_data.sort((a, b) => b.foldchange - a.foldchange); - if ($("#statistical_test").val()) - selected_gene_data.sort((a, b) => a.pvals - b.pvals); + Plotly.newPlot("plotly_preview", plotData, layout, config); - const template = $.templates("#selected_genes_tmpl"); - const htmlOutput = template.render(selected_gene_data); - $("#selected_genes_c").html(htmlOutput); + // If plot data is selected, create the right-column table and do other misc things + plotlyPreview.on("plotly_selected", (eventData) => { + + // Hide selected genes table and disable unweighted radio button if no genes are selected + document.getElementById("tbl_selected_genes").classList.add("is-hidden"); + document.getElementById("download_selected_genes_btn").classList.add("is-hidden"); + document.querySelector("input[name='genecart_type'][value='unweighted']").disabled = true; + document.querySelector("input[name='genecart_type'][value='unweighted']").parentElement.setAttribute("disabled", "disabled"); + + if (eventData?.points.length) { + document.getElementById("tbl_selected_genes").classList.remove("is-hidden"); + document.getElementById("download_selected_genes_btn").classList.remove("is-hidden"); + document.querySelector("input[name='genecart_type'][value='unweighted']").disabled = false; + document.querySelector("input[name='genecart_type'][value='unweighted']").parentElement.removeAttribute("disabled"); + + adjustGeneTableLabels(); + populateGeneTable(eventData); + } + + // Get genes from gene tags + const geneTags = document.querySelectorAll("#gene_tags span.tag"); + const searchedGenes = []; + for (const tag of geneTags) { + searchedGenes.push(tag.textContent); + } // Highlight table rows that match searched genes - if ($('#highlighted_genes').val()) { - const searched_genes = $('#highlighted_genes').val().replace(/\s/g, "").split(","); + if (searchedGenes) { + const geneTableBody = document.getElementById("gene_table_body"); // Select the first column (gene_symbols) in each row - $("#selected_genes_c tr td:first-child").each(function() { - const table_gene = $(this).text(); - searched_genes.forEach((gene) => { - if (gene.toLowerCase() === table_gene.toLowerCase() ) { - $(this).parent().addClass("table-success"); + for (const row of geneTableBody.children) { + const tableGene = row.children[0].textContent; + for (const gene of searchedGenes) { + if (gene.toLowerCase() === tableGene.toLowerCase() ) { + row.classList.add("has-background-success-light"); } - }); - }) + }; + } } + }); - // toggle visibilities - $("#selection_methods_c").hide(); - $("#saved_gene_cart_info_c").hide(); - $("#gene_list_c").show(); - - $("#controls_label").hide(); - $("#selected_label").show(); - + // Handler for when plot text is edited + plotlyPreview.on("plotly_relayout", (eventData) => { + // If plot title, x-axis or y-axis is edited, save the text + if (eventData?.["title.text"]) { + titleText = eventData["title.text"]; + } + if (eventData?.["xaxis.title.text"]) { + xaxisText = eventData["xaxis.title.text"]; + } + if (eventData?.["yaxis.title.text"]) { + yaxisText = eventData["yaxis.title.text"]; + } }); - window.onresize = () => { - Plotly.Plots.resize(graphDiv); - }; + const plotlyNote = document.createElement("div"); + plotlyNote.id = "tip_on_editing"; + plotlyNote.classList.add("notification", "content", "is-info", "is-light"); + plotlyNote.innerHTML = `Tip: Use the Plotly box and lasso select tools (upper-right) to select genes to view as a table.
+ +You can also click the plot title or axis labels to edit them. Hit Enter to apply edit.
`; + plotlyPreview.append(plotlyNote); } -function save_gene_cart() { - // must have access to USER_SESSION_ID - const gc = new GeneCart({ - session_id: CURRENT_USER.session_id, - label: $("#gene_cart_name").val(), - gctype: 'unweighted-list', - organism_id: $("#dataset_id").data('organism-id'), - is_public: 0 - }); - selected_data.points.forEach((pt) => { - const gene = new Gene({ - id: plot_data.gene_ids[pt.pointNumber], - gene_symbol: plot_data.symbols[pt.pointNumber], +const populateGeneTable = (data) => { + const statisticalTest = document.getElementById("statistical_test").value; + + selectedGeneData = []; + + data.points.forEach((pt) => { + // Some warnings on using toFixed() here: https://stackoverflow.com/a/12698296/1368079 + // Each trace has its own "pointNumber" ids so gene symbols and pvalues needed to be passed in for each plotdata trace + selectedGeneData.push({ + gene_symbol: pt.data.id[pt.pointNumber], + pval: statisticalTest ? pt.data.pvals[pt.pointNumber].toExponential(2) : "NA", + foldchange: pt.data.foldchange[pt.pointNumber].toFixed(1), + x: pt.data.x[pt.pointNumber].toFixed(1), + y: pt.data.y[pt.pointNumber].toFixed(1) }); - gc.add_gene(gene); }); - gc.save(update_ui_after_gene_cart_save_success, update_ui_after_gene_cart_save_failure); + // Sort by adjusted p-value in descending order either by fold change or p-values + selectedGeneData.sort((a, b) => b.foldchange - a.foldchange); + if (statisticalTest) + selectedGeneData.sort((a, b) => a.pval - b.pval); + + + const geneTableBody = document.getElementById("gene_table_body"); + geneTableBody.replaceChildren(); + + for (const gene of selectedGeneData) { + const row = document.createElement("tr"); + row.innerHTML = `Tip: Use the Plotly box and lasso select tools (upper-right) to select genes to view as a table.
- -You can also click the plot title or axis labels to edit them. Hit Enter to apply edit.
`; - plotlyPreview.append(plotlyNote); -} - - -const populateGeneTable = (data) => { - const statisticalTest = document.getElementById("statistical_test").value; - - selectedGeneData = []; - - data.points.forEach((pt) => { - // Some warnings on using toFixed() here: https://stackoverflow.com/a/12698296/1368079 - // Each trace has its own "pointNumber" ids so gene symbols and pvalues needed to be passed in for each plotdata trace - selectedGeneData.push({ - gene_symbol: pt.data.id[pt.pointNumber], - pval: statisticalTest ? pt.data.pvals[pt.pointNumber].toExponential(2) : "NA", - foldchange: pt.data.foldchange[pt.pointNumber].toFixed(1), - x: pt.data.x[pt.pointNumber].toFixed(1), - y: pt.data.y[pt.pointNumber].toFixed(1) - }); - }); - - // Sort by adjusted p-value in descending order either by fold change or p-values - selectedGeneData.sort((a, b) => b.foldchange - a.foldchange); - if (statisticalTest) - selectedGeneData.sort((a, b) => a.pval - b.pval); - - - const geneTableBody = document.getElementById("gene_table_body"); - geneTableBody.replaceChildren(); - - for (const gene of selectedGeneData) { - const row = document.createElement("tr"); - row.innerHTML = `Add Display
-Loading
-
-
-