diff --git a/main/html/index.html b/main/html/index.html index c85f89b6..bbf82b8d 100644 --- a/main/html/index.html +++ b/main/html/index.html @@ -32,7 +32,7 @@ - + @@ -127,7 +127,7 @@
- +
diff --git a/main/js/afterSubmit.js b/main/js/afterSubmit.js index 4c160843..40d2cd5c 100644 --- a/main/js/afterSubmit.js +++ b/main/js/afterSubmit.js @@ -56,35 +56,21 @@ const buildPlots = async function() { // GET EXPRESSION DATA: const selectedGene1 = $(".geneOneMultipleSelection").select2("data").map((gene) => gene.text); - // Find intersecting barcodes based on Mutation/Clinical Pie Chart selections - const pieSectorBarcodes = await getBarcodesFromSelectedPieSectors(selectedTumorTypes); - const histogramBarcodes = await getBarcodesFromSelectedHistogramRange(selectedTumorTypes); - console.log(pieSectorBarcodes, histogramBarcodes); - let intersectedBarcodes; - if(pieSectorBarcodes && histogramBarcodes) { - intersectedBarcodes = pieSectorBarcodes.reduce((acc, curr) => { - if (histogramBarcodes.includes(curr)) { - acc.push(curr); - } - return acc; - }, []); - } else if (pieSectorBarcodes) { - intersectedBarcodes = pieSectorBarcodes; - } else if (histogramBarcodes) { - intersectedBarcodes = histogramBarcodes; - } else { - intersectedBarcodes = null; - } - console.log(intersectedBarcodes); + let cacheGe = await getCacheGE(); // Instantiate cache interface for gene expression let expressionData; + // GET CLINICAL DATA: // Get clinical data for either intersected barcodes or entire cohort let clinicalData; let cacheBar = await getCacheBAR(); // Instantiate cache interface for barcodes let barcodesByCohort = await cacheBar.fetchWrapperBAR(selectedTumorTypes); // Fetch all barcodes for selected cohorts let cacheClin = await getCacheCLIN(); // Instantiate cache interface for clinical data - if (intersectedBarcodes && intersectedBarcodes.length) { + + let intersectedBarcodes = await getBarcodesFromSelectedFeatures(selectedTumorTypes); + + if (intersectedBarcodes.length > 0) { + // If intersectedBarcodes is populated, then iterate over each cohort's barcodes and filter by the barcodes of interest for(let index = 0; index < barcodesByCohort.length; index++) { let obj = barcodesByCohort[index]; @@ -99,6 +85,9 @@ const buildPlots = async function() { // Pass in barcodes from expressionData clinicalData = await cacheClin.fetchWrapperCLIN(selectedTumorTypes, barcodesByCohort); // Fetch clinical data from cache } + expressionData = (expressionData || []).filter( + r => r && allSelectedGenes.includes(r.gene) + ); cache.set('rnaSeq', 'expressionData', expressionData); // Set localStorage entry for expression data clinicalData = clinicalData.map(obj => obj.clinical_data); // Extract clinical_data property from each element clinicalData = clinicalData.flat(); // Flatten clinicalData into a 1-D array @@ -110,6 +99,7 @@ const buildPlots = async function() { let mutationAndClinicalData = mergeClinicalAndMutationData(selectedGene1, mutationData, clinicalData); // Combine mutation data and clinical data into single array of JSON objects localStorage.setItem("mutationAndClinicalData", JSON.stringify(mutationAndClinicalData)); localStorage.setItem("mutationAndClinicalFeatureKeys", Object.keys((mutationAndClinicalData[0])).sort()); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// buildHeatmap(expressionData, mutationAndClinicalData); buildViolinPlot(allSelectedGenes, expressionData); diff --git a/main/js/dataAcquisition/getDataFromSelectedFeatures.js b/main/js/dataAcquisition/getDataFromSelectedFeatures.js new file mode 100644 index 00000000..63520cd8 --- /dev/null +++ b/main/js/dataAcquisition/getDataFromSelectedFeatures.js @@ -0,0 +1,192 @@ + +// ***** Get intersection of barcodes from selections in data explore charts (below) ***** + +getBarcodesFromSelectedFeatures = async function(selectedTumorTypes) { + + // retrieve selection data from global variables + let selectedCategoricalFields = Object.keys(selectedCategoricalFeatures); + let selectedContinuousFields = Object.keys(selectedContinuousFeatures); + + let barcodesReppingAllSelectionsForEachFeature = []; + let cacheMu = await getCacheMU(); // Instantiate caching interface for mutation data + let cacheBar = await getCacheBAR(); // Instantiate caching interface for barcode data + let cacheClin = await getCacheCLIN(); // Instantiate caching interface for clinical data + + let allBarcodesForSelectedTumorType = await cacheBar.fetchWrapperBAR(selectedTumorTypes); // Get all barcodes for the selected tumor type(s) + + let allClinicalDataForSelectedTumorTypes = await cacheClin.fetchWrapperCLIN(selectedTumorTypes, allBarcodesForSelectedTumorType); // Fetch clinical data for all patients with selected tumor type(s) + allClinicalDataForSelectedTumorTypes = allClinicalDataForSelectedTumorTypes.map(obj => obj.clinical_data); // Extract mutation_data property for each cohort + allClinicalDataForSelectedTumorTypes = allClinicalDataForSelectedTumorTypes.flat(); // Use flat() to make patients' clinical data a 1-D array + + // LOOP THRU ALL CLICKED ~CATEGORICAL~ FIELDS (whether from gene mutation plot or metadata) + // (e.g., one gene at a time, or one metadata field at a time) + for(let i = 0; i < selectedCategoricalFields.length; i++) { + + let currentField = selectedCategoricalFields[i]; + + // if current selected sector belongs to a gene... + if(currentField[i].toUpperCase() == currentField[i]) { + + let currentGene = currentField; + + let allMutationDataForThisGene = await cacheMu.fetchWrapperMU(selectedTumorTypes, [currentGene]); // Fetch all mutation data for currentGene + + let clickedMutations = selectedCategoricalFeatures[currentGene]; // Get array of selected mutations + + barcodesReppingAllSelectionsForEachFeature[currentGene+'mutationFilt'] = []; // Initialize to empty array to use push() + + if(clickedMutations.length > 0) { + barcodesReppingAllSelectionsForEachFeature[currentGene+'mutationFilt'] = allMutationDataForThisGene + .filter(record => clickedMutations.includes(record.mutation_label)) + .map(record => record.tcga_participant_barcode); + + } + // We are basically getting lists of barcodes, repping ppl who have particular mutations in particular genes + + // If no mutations have been selected, then we will append all the barcodes to the array + // else { + // barcodesReppingAllSelectionsForEachFeature[currentGene] = allMutationDataForThisGene.map(record => record.tcga_participant_barcode); + // } + + // ELSE, CURRENT CATEGORICAL FIELD IS A "CLINICAL" ONE + } else { + + let currentClinicalFeature = currentField; + let clickedClinicalValues = selectedCategoricalFeatures[currentClinicalFeature]; + + let filteredClinicalData = []; + let uniqueBarcodes; + + function onlyUnique(value, index, self) { + return self.indexOf(value) === index; + } + // for each value of the + for(let j = 0; j < clickedClinicalValues.length; j++) { + + let currentClinicalValue = clickedClinicalValues[j]; + + // filter the clinical data object to only include entries (i.e., barcodes) that have the selected values for the clinical data field + filteredClinicalData = allClinicalDataForSelectedTumorTypes.filter(person => (person[currentClinicalFeature] == currentClinicalValue)) + // now just get the barcodes from those entries + let onlyBarcodes = filteredClinicalData.map(x => x.tcga_participant_barcode); + uniqueBarcodes = onlyBarcodes.filter(onlyUnique); + + if (barcodesReppingAllSelectionsForEachFeature[currentClinicalFeature] === undefined) + barcodesReppingAllSelectionsForEachFeature[currentClinicalFeature] = uniqueBarcodes; + else + barcodesReppingAllSelectionsForEachFeature[currentClinicalFeature] = barcodesReppingAllSelectionsForEachFeature[currentClinicalFeature].concat(uniqueBarcodes); + } + } + } + + // LOOP THRU ALL CLICKED ~CONTINUOUS~ FIELDS (whether from gene mutation plot or metadata) + // (e.g., one gene at a time, or one metadata field at a time) + for(let i = 0; i < selectedContinuousFields.length; i++) { + + let currentField = selectedContinuousFields[i]; + + let rangeValue = selectedContinuousFeatures[currentField]; // Get range of data to filter allClinicalDataForSelectedTumorTypes by + let onlyBarcodes = []; + + // if current continuous field is a GENE + if (currentField[0] === currentField[0].toUpperCase()) { + + // Fetch gene expression cache + let cacheGe = await getCacheGE(); + + let genesWithExpressionFilters = Object.keys(selectedContinuousFeatures).filter(key => { + // Check if this key represents a gene (starts with uppercase) and has a range + return key[0] === key[0].toUpperCase() && + selectedContinuousFeatures[key] && + Array.isArray(selectedContinuousFeatures[key]) && + selectedContinuousFeatures[key].length >= 2; + }); + // Process each gene with expression filters + for (let gene of genesWithExpressionFilters) { + let rangeValue = selectedContinuousFeatures[gene]; + let minExpression = rangeValue[0]; + let maxExpression = rangeValue[1]; + + // console.log(`Processing expression filter for ${gene}: ${minExpression} to ${maxExpression}`); + + // Fetch gene expression data for this gene + let geneExpressionData = await cacheGe.fetchWrapperGE(selectedTumorTypes, [gene]); + + // Filter by expression range and tumor samples only + let filteredExpressionData = geneExpressionData.filter(record => { + return record.sample_type === "TP" && + record.expression_log2 !== null && + record.expression_log2 !== undefined && + !isNaN(record.expression_log2) && + record.expression_log2 >= minExpression && + record.expression_log2 <= maxExpression; + }); + + + // Extract unique barcodes for this gene + onlyBarcodes = filteredExpressionData.map(record => record.tcga_participant_barcode); + + barcodesReppingAllSelectionsForEachFeature[gene+'expressionFilt'] = onlyBarcodes + + } + + } else { + + filteredRangeData = allClinicalDataForSelectedTumorTypes.filter(person => (person[currentField] >= rangeValue[0] && person[currentField] <= rangeValue[1])) + onlyBarcodes = filteredRangeData.map(x => x.tcga_participant_barcode); + barcodesReppingAllSelectionsForEachFeature[currentField] = onlyBarcodes + + } + } + + // console.log(barcodesReppingAllSelectionsForEachFeature) + + function intersectValues(kv) { + let arrays; + + if (kv instanceof Map) { + arrays = Array.from(kv.values()); + } else if (Array.isArray(kv)) { + // Handle arrays with named properties (Object.values grabs them) + const vals = Object.values(kv); + // If entries-like [[key, arr], ...], pick the arr + if (vals.every(v => Array.isArray(v) && v.length === 2 && Array.isArray(v[1]))) { + arrays = vals.map(v => v[1]); + } else { + arrays = vals.filter(Array.isArray); // named props or array-of-arrays + if (arrays.length === 0) arrays = kv.filter?.(Array.isArray) ?? []; + } + } else if (kv && typeof kv === 'object') { + arrays = Object.values(kv).filter(Array.isArray); + } else { + return []; + } + + if (!arrays.length || arrays.some(a => a.length === 0)) return []; + + // Dedupe each, start with smallest, intersect via Sets + const deduped = arrays.map(a => { + const s = new Set(); const out = []; + for (const x of a) if (!s.has(x)) { s.add(x); out.push(x); } + return out; + }).sort((a,b) => a.length - b.length); + + const [first, ...rest] = deduped; + const restSets = rest.map(a => new Set(a)); + + const out = []; + const seen = new Set(); + for (const x of first) { + if (!seen.has(x) && restSets.every(s => s.has(x))) { + seen.add(x); + out.push(x); + } + } + return out; + } + + const common = intersectValues(barcodesReppingAllSelectionsForEachFeature); + // console.log(common); + + return common +} \ No newline at end of file diff --git a/main/js/dataAcquisition/getDataFromSelectedPieSectors.js b/main/js/dataAcquisition/getDataFromSelectedPieSectors.js deleted file mode 100644 index 2d54a8d2..00000000 --- a/main/js/dataAcquisition/getDataFromSelectedPieSectors.js +++ /dev/null @@ -1,196 +0,0 @@ - -// ***** Get intersection of barcodes from selected pie sectors (below) ***** - -getBarcodesFromSelectedPieSectors = async function(selectedTumorTypes) { - // a "field" is either a gene name or a clinical feature - let selectedCategoricalFields = Object.keys(selectedCategoricalFeatures); - let concatFilteredBarcodes = []; - let cacheMu = await getCacheMU(); // Instantiate caching interface for mutation data - let cacheBar = await getCacheBAR(); // Instantiate caching interface for barcode data - let cacheClin = await getCacheCLIN(); // Instantiate caching interface for clinical data - let barcodesByCohort = await cacheBar.fetchWrapperBAR(selectedTumorTypes); // Get all barcodes for the selected cohorts - let clinicalData = await cacheClin.fetchWrapperCLIN(selectedTumorTypes, barcodesByCohort); // Fetch clinical data for cohorts of interest - clinicalData = clinicalData.map(obj => obj.clinical_data); // Extract mutation_data property for each cohort - clinicalData = clinicalData.flat(); // Use flat() to make patients' clinical data a 1-D array - // LOOP THRU ALL CLICKED FIELDS - for(let i = 0; i < selectedCategoricalFields.length; i++) { - let currentField = selectedCategoricalFields[i]; - // if current selected sector belongs to a gene... - if(currentField[i].toUpperCase() == currentField[i]) { - let currentGene = currentField; - let mutationDataForThisGene = await cacheMu.fetchWrapperMU(selectedTumorTypes, [currentGene]); // Fetch mutation data for currentGene - let clickedMutations = selectedCategoricalFeatures[currentGene]; // Get array of selected mutations - concatFilteredBarcodes[currentGene] = []; // Initialize to empty array to use push() - //If mutations have been selected, then append the relevant barcodes - if(clickedMutations.length > 0) { - // Iterate over mutation data for a specific gene to get patients with mutation types of interest - for(let index = 0; index < mutationDataForThisGene.length; index++) { - // If mutation_label property for current patient is in array of selected mutation types, then append to barcodes array - if(clickedMutations.includes(mutationDataForThisGene[index].mutation_label)) - concatFilteredBarcodes[currentGene].push(mutationDataForThisGene[index]["tcga_participant_barcode"]); // Append patient barcode to concatFilteredBarcodes - } - } - //If no mutations have been selected, then we will append all the barcodes to the array - else { - for(let index = 0; index < mutationDataForThisGene.length; index++) { - concatFilteredBarcodes[currentGene].push(mutationDataForThisGene[index]["tcga_participant_barcode"]); // Apend patient barcode to concatFilteredBarcodes - } - } - - } else { - - let currentClinicalFeature = currentField; - let filteredClinicalData = []; - let uniqueBarcodes; - - let clickedClinicalValues = selectedCategoricalFeatures[currentClinicalFeature]; - - for(let j = 0; j < clickedClinicalValues.length; j++) { - - let currentClinicalValue = clickedClinicalValues[j]; - - filteredClinicalData = clinicalData.filter(person => (person[currentClinicalFeature] == currentClinicalValue)) - - let onlyBarcodes = filteredClinicalData.map(x => x.tcga_participant_barcode); - - function onlyUnique(value, index, self) { - return self.indexOf(value) === index; - } - uniqueBarcodes = onlyBarcodes.filter(onlyUnique); - - if(concatFilteredBarcodes['' + currentClinicalFeature] == undefined) - concatFilteredBarcodes['' + currentClinicalFeature] = uniqueBarcodes; - else - concatFilteredBarcodes['' + currentClinicalFeature] = concatFilteredBarcodes['' + currentClinicalFeature].concat(uniqueBarcodes); - } - } - } - // loop through all range data - for(let continuousFeature of Object.keys(selectedContinuousFeatures)) { - let rangeValue = selectedContinuousFeatures[continuousFeature]; // Get range of data to filter clinicalData by - filteredRangeData = clinicalData.filter(person => (person[continuousFeature] >= rangeValue[0] && person[continuousFeature] <= rangeValue[1])) - - let onlyBarcodes = filteredRangeData.map(x => x.tcga_participant_barcode); - - function onlyUnique(value, index, self) { - return self.indexOf(value) === index; - } - uniqueBarcodes = onlyBarcodes.filter(onlyUnique); - - if(concatFilteredBarcodes['' + continuousFeature] == undefined) - concatFilteredBarcodes['' + continuousFeature] = uniqueBarcodes; - else - concatFilteredBarcodes['' + continuousFeature] = concatFilteredBarcodes['' + continuousFeature].concat(uniqueBarcodes); - } - - - // Get intersection of barcodes from selected pie sectors - let clicked_gene_mutation = Object.keys(concatFilteredBarcodes); - let intersectedBarcodes; - - // If user clicked 0 or 1 gene/mutation combos, simply use these barcodes - if(clicked_gene_mutation.length <= 1) { - let currentGene = clicked_gene_mutation[0]; - intersectedBarcodes = concatFilteredBarcodes[currentGene]; // barcode(s) for selected gene mutation combo in given cancer type - - // If user clicked >1 gene/mutation combos, compute intersection - } else { - for(let i = 0; i < clicked_gene_mutation.length - 1; i++) { - let currentGene = clicked_gene_mutation[i]; - let nextGene = clicked_gene_mutation[i + 1]; - let barcodesForCurrentGene = concatFilteredBarcodes[currentGene]; // barcode(s) for selected gene mutation combo in given cancer type - let barcodesForNextGene = concatFilteredBarcodes[nextGene]; - intersectedBarcodes = barcodesForCurrentGene.filter(x => barcodesForNextGene.includes(x)); - } - } - return intersectedBarcodes -} - -/** - * Get patient barcodes from selected histogram expression ranges - * - * @param {Array} selectedTumorTypes - Array of selected tumor types - * @returns {Array} Array of patient barcodes that match expression range criteria - */ -async function getBarcodesFromSelectedHistogramRange(selectedTumorTypes) { - let histogramFilteredBarcodes = []; - - // Get all genes that have expression range filters - console.log(selectedContinuousFeatures) - let genesWithExpressionFilters = Object.keys(selectedContinuousFeatures).filter(key => { - // Check if this key represents a gene (starts with uppercase) and has a range - return key[0] === key[0].toUpperCase() && - selectedContinuousFeatures[key] && - selectedContinuousFeatures[key].length >= 2; - }); - - console.log('Genes with expression filters:', genesWithExpressionFilters); - - if (genesWithExpressionFilters.length === 0) { - return []; // No expression filters applied - } - - // Fetch gene expression cache - let cacheGe = await getCacheGE(); - - // Process each gene with expression filters - for (let gene of genesWithExpressionFilters) { - let rangeValue = selectedContinuousFeatures[gene]; - let minExpression = rangeValue[0]; - let maxExpression = rangeValue[1]; - - console.log(`Processing expression filter for ${gene}: ${minExpression} to ${maxExpression}`); - - // Fetch gene expression data for this gene - let geneExpressionData = await cacheGe.fetchWrapperGE(selectedTumorTypes, [gene]); - - // Filter by expression range and tumor samples only - let filteredExpressionData = geneExpressionData.filter(record => { - return record.sample_type === "TP" && - record.expression_log2 !== null && - record.expression_log2 !== undefined && - !isNaN(record.expression_log2) && - record.expression_log2 >= minExpression && - record.expression_log2 <= maxExpression; - }); - - // Extract unique barcodes for this gene - let barcodesForThisGene = filteredExpressionData.map(record => record.tcga_participant_barcode); - - // Remove duplicates - function onlyUnique(value, index, self) { - return self.indexOf(value) === index; - } - barcodesForThisGene = barcodesForThisGene.filter(onlyUnique); - - console.log(`Found ${barcodesForThisGene.length} patients for ${gene} expression filter`); - - // Store barcodes for this gene - histogramFilteredBarcodes.push({ - gene: gene, - range: [minExpression, maxExpression], - barcodes: barcodesForThisGene - }); - } - - // If only one gene filter, return those barcodes - if (histogramFilteredBarcodes.length === 1) { - return histogramFilteredBarcodes[0].barcodes; - } - - // If multiple gene filters, compute intersection - if (histogramFilteredBarcodes.length > 1) { - let intersectedBarcodes = histogramFilteredBarcodes[0].barcodes; - - for (let i = 1; i < histogramFilteredBarcodes.length; i++) { - intersectedBarcodes = intersectedBarcodes.filter(barcode => - histogramFilteredBarcodes[i].barcodes.includes(barcode) - ); - } - - console.log(`Intersection of ${histogramFilteredBarcodes.length} expression filters: ${intersectedBarcodes.length} patients`); - return intersectedBarcodes; - } - - return []; -} diff --git a/main/js/plots/createPieCharts.js b/main/js/plots/createDataExplorePlots.js similarity index 72% rename from main/js/plots/createPieCharts.js rename to main/js/plots/createDataExplorePlots.js index 80f67305..938d20e8 100644 --- a/main/js/plots/createPieCharts.js +++ b/main/js/plots/createDataExplorePlots.js @@ -188,8 +188,9 @@ let buildDataExplorePlots = async function() { uniqueValuesForCurrentFeature = Array.from(mutationCounts.keys()); // Get mutation types from keys() xCounts = Array.from(mutationCounts.values()); // Get corresponding counts from values() let cacheGe = await getCacheGE(); - let geneMutationExpression = await cacheGe.fetchWrapperGE(selectedTumorTypes, [currentFeature]); + let geneMutationExpression = await cacheGe.fetchWrapperGE(selectedTumorTypes, [currentFeature]); await createGeneExpressionHistogram(geneMutationExpression, mutationData, currentFeature); + // if current feature is clinical (i.e., not a gene) // get values and labels for this feature } else { @@ -288,16 +289,14 @@ let computeMutationFrequencies = function(mutationData) { let computeClinicalFeatureFrequencies = async function (xCounts, uniqueValuesForCurrentFeature, currentClinicalFeatureSelected, continuous) { let allValuesForCurrentFeature = []; - console.log(allClinicalData) for(let i = 0; i < allClinicalData.length; i++) allValuesForCurrentFeature.push(allClinicalData[i][currentClinicalFeatureSelected]); let index = clinicalType.findIndex(x => x.name == currentClinicalFeatureSelected); clinicalType[index].isSelected = true; - console.log(clinicalType[index]) if (clinicalType[index].type === "continuous") { continuous = true; - uniqueValuesForCurrentFeature = allValuesForCurrentFeature; // changed from uniqueValuesForCurrentFeature = allValuesForCurrentFeature.filter(onlyUnique); + uniqueValuesForCurrentFeature = allValuesForCurrentFeature; } else { continuous = false; uniqueValuesForCurrentFeature = allValuesForCurrentFeature.filter(onlyUnique); @@ -409,8 +408,6 @@ let setChartDimsAndPlot = async function (uniqueValuesForCurrentFeature, current 'Frequency: %{y}', type: 'histogram' }]; - console.log(histo_data) - // set colors of pie sectors: if (!continuous) { @@ -463,7 +460,6 @@ let setChartDimsAndPlot = async function (uniqueValuesForCurrentFeature, current bargap: 0.05, height: 400, width: 500, - // title: (currentFeature + "").replaceAll('_', ' '), showlegend: false, font: { family: 'Arial, Helvetica, sans-serif' @@ -536,14 +532,6 @@ async function createGeneExpressionHistogram(geneMutationExpression, mutationDat x: expressionValues, type: 'histogram', name: `${currentFeature} Expression`, - opacity: 0.8, - marker: { - color: '#3498db', // Single blue color - line: { - color: 'black', - width: 1 - } - }, xbins: { size: calculateBinSize(expressionValues) }, @@ -552,12 +540,17 @@ async function createGeneExpressionHistogram(geneMutationExpression, mutationDat // Create histogram layout with native range selection const histogramLayout = { - title: `${currentFeature} Expression Distribution
Use range selector below to filter by expression level`, + bargap: 0.05, + height: 400, + width: 500, + dragmode: 'select', + selectdirection: 'h', + title: `${currentFeature} Expression Distribution`, xaxis: { title: 'Expression Level (log2)', tickfont: { size: 12 }, rangeslider: { - visible: true, + visible: false, thickness: 0.1 }, rangeselector: { @@ -576,8 +569,6 @@ async function createGeneExpressionHistogram(geneMutationExpression, mutationDat title: 'Frequency', tickfont: { size: 12 } }, - height: 500, // Increased to accommodate range slider - width: 600, font: { family: 'Arial, Helvetica, sans-serif' } @@ -594,216 +585,129 @@ async function createGeneExpressionHistogram(geneMutationExpression, mutationDat histogramDiv.setAttribute("class", "col s6"); parentDiv.appendChild(histogramDiv); } - - // Add filter status div - let filterStatusDiv = document.getElementById(currentFeature + "FilterStatus"); - if (!filterStatusDiv) { - filterStatusDiv = document.createElement("div"); - filterStatusDiv.setAttribute("id", currentFeature + "FilterStatus"); - filterStatusDiv.setAttribute("style", "margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-radius: 5px; text-align: center;"); - filterStatusDiv.innerHTML = `Current Filter: All patients (${expressionValues.length})`; - histogramDiv.appendChild(filterStatusDiv); - } - // Plot the histogram + const divId = currentFeature + "ExpressionDiv"; + const gd = document.getElementById(divId); + const plotConfig = { - responsive: true, - displayModeBar: true, - modeBarButtonsToRemove: ['pan2d', 'lasso2d', 'select2d', 'autoScale2d'], - scrollZoom: false + responsive: true, + displayModeBar: true, + modeBarButtonsToRemove: ['pan2d', 'lasso2d', 'select2d', 'autoScale2d'], + scrollZoom: false }; - - Plotly.newPlot(currentFeature + "ExpressionDiv", [histogramTrace], histogramLayout, plotConfig); - - // Add event listener for range changes - document.getElementById(currentFeature + "ExpressionDiv").on('plotly_relayout', function(eventData) { - // Handle range slider changes or zoom changes - if (eventData['xaxis.range[0]'] !== undefined || eventData['xaxis.range[1]'] !== undefined) { - const minRange = eventData['xaxis.range[0]'] || Math.min(...expressionValues); - const maxRange = eventData['xaxis.range[1]'] || Math.max(...expressionValues); - - // Count patients in selected range - const filteredCount = expressionValues.filter(val => - val >= minRange && val <= maxRange - ).length; + + Plotly.newPlot(gd, [histogramTrace], histogramLayout, plotConfig).then(gd => { + const dataMin = Math.min(...expressionValues); + const dataMax = Math.max(...expressionValues); + + const updateStateAndUI = (minVal, maxVal, isReset=false) => { + // 1) Update global state FIRST + const isFullRange = Math.abs(minVal - dataMin) < 0.01 && Math.abs(maxVal - dataMax) < 0.01; + if (isFullRange || isReset) { + getExpressionRangeValues(currentFeature, null); + } else { + getExpressionRangeValues(currentFeature, { min: minVal, max: maxVal }); + } + + // 2) Then update UI (guard null) + const rangeDisplay = document.getElementById(currentFeature + "RangeDisplay"); + if (rangeDisplay) { + if (isFullRange || isReset) { + rangeDisplay.textContent = `All patients (${expressionValues.length})`; + } else { + const filteredCount = expressionValues.filter(v => v >= minVal && v <= maxVal).length; + rangeDisplay.textContent = + `Range: ${minVal.toFixed(2)} to ${maxVal.toFixed(2)} (${filteredCount} patients)`; + } + } + }; + + // Selection (drag box) + gd.on('plotly_selected', ev => { + const [minVal, maxVal] = ev?.range?.x ?? [dataMin, dataMax]; + updateStateAndUI(minVal, maxVal); + }); + + // Deselection (click empty space) + gd.on('plotly_deselect', () => updateStateAndUI(dataMin, dataMax, true)); + + // Zoom/pan/range changes + gd.on('plotly_relayout', ev => { + const r0 = ev['xaxis.range[0]'] ?? ev['xaxis.range']?.[0]; + const r1 = ev['xaxis.range[1]'] ?? ev['xaxis.range']?.[1]; + + if (r0 !== undefined && r1 !== undefined) { + updateStateAndUI(r0, r1); + } + if (ev['xaxis.autorange'] === true) { + updateStateAndUI(dataMin, dataMax, true); + } + }); + }); + + /** + * Apply expression level filter to the patient cohort + * This function integrates with the existing filtering system used by pie charts + * + * @param {String} gene - The gene being filtered + * @param {Object|null} range - The expression range {min, max} or null to clear filter + */ + function getExpressionRangeValues(gene, range) { + if (range) { + console.log(`Applying expression filter for ${gene}: ${range.min.toFixed(2)} to ${range.max.toFixed(2)}`); + + // Store the range in selectedContinuousFeatures following the same pattern as pie charts + selectedContinuousFeatures[gene] = [range.min, range.max]; - // Update filter status display - const rangeDisplay = document.getElementById(currentFeature + "RangeDisplay"); + console.log('Updated selectedContinuousFeatures:', selectedContinuousFeatures); - // Check if this is essentially the full range (no real filtering) - const dataMin = Math.min(...expressionValues); - const dataMax = Math.max(...expressionValues); - const isFullRange = (Math.abs(minRange - dataMin) < 0.01) && (Math.abs(maxRange - dataMax) < 0.01); + } else { + console.log(`Clearing expression filter for ${gene}`); - if (isFullRange) { - rangeDisplay.textContent = `All patients (${expressionValues.length})`; - applyExpressionFilter(currentFeature, null); - } else { - rangeDisplay.textContent = `Range: ${minRange.toFixed(2)} to ${maxRange.toFixed(2)} (${filteredCount} patients)`; - applyExpressionFilter(currentFeature, {min: minRange, max: maxRange}); + // Remove the filter from selectedContinuousFeatures + if (selectedContinuousFeatures[gene]) { + delete selectedContinuousFeatures[gene]; } + + console.log('Updated selectedContinuousFeatures:', selectedContinuousFeatures); } - - // Handle range slider reset - if (eventData['xaxis.autorange'] === true) { - const rangeDisplay = document.getElementById(currentFeature + "RangeDisplay"); - rangeDisplay.textContent = `All patients (${expressionValues.length})`; - applyExpressionFilter(currentFeature, null); - } - }); -} - -/** - * Apply expression level filter to the patient cohort - * This function integrates with the existing filtering system used by pie charts - * - * @param {String} gene - The gene being filtered - * @param {Object|null} range - The expression range {min, max} or null to clear filter - */ -function applyExpressionFilter(gene, range) { - if (range) { - console.log(`Applying expression filter for ${gene}: ${range.min.toFixed(2)} to ${range.max.toFixed(2)}`); - - // Store the range in selectedContinuousFeatures following the same pattern as pie charts - selectedContinuousFeatures[gene] = [range.min, range.max]; - - console.log('Updated selectedContinuousFeatures:', selectedContinuousFeatures); - - } else { - console.log(`Clearing expression filter for ${gene}`); - - // Remove the filter from selectedContinuousFeatures - if (selectedContinuousFeatures[gene]) { - delete selectedContinuousFeatures[gene]; - } - - console.log('Updated selectedContinuousFeatures:', selectedContinuousFeatures); } -} -/** - * Get patient barcodes from selected histogram expression ranges - * - * @param {Array} selectedTumorTypes - Array of selected tumor types - * @returns {Array} Array of patient barcodes that match expression range criteria - */ -async function getBarcodesFromSelectedHistogramRange(selectedTumorTypes) { - let histogramFilteredBarcodes = []; - - // Get all genes that have expression range filters - let genesWithExpressionFilters = Object.keys(selectedContinuousFeatures).filter(key => { - // Check if this key represents a gene (starts with uppercase) and has a range - return key[0] === key[0].toUpperCase() && - selectedContinuousFeatures[key] && - Array.isArray(selectedContinuousFeatures[key]) && - selectedContinuousFeatures[key].length >= 2; - }); - - console.log('Genes with expression filters:', genesWithExpressionFilters); - console.log('Current selectedContinuousFeatures:', selectedContinuousFeatures); - - if (genesWithExpressionFilters.length === 0) { - return []; // No expression filters applied - } - - // Fetch gene expression cache - let cacheGe = await getCacheGE(); - - // Process each gene with expression filters - for (let gene of genesWithExpressionFilters) { - let rangeValue = selectedContinuousFeatures[gene]; - let minExpression = rangeValue[0]; - let maxExpression = rangeValue[1]; + /** + * Helper function to calculate appropriate bin size based on data distribution + * + * @param {Array} data - Array of expression values + * @returns {Number} - Appropriate bin size + */ + function calculateBinSize(data) { + // Calculate IQR (Interquartile Range) based bin size using Freedman-Diaconis rule + if (data.length < 2) return 0.5; // Default if not enough data - console.log(`Processing expression filter for ${gene}: ${minExpression} to ${maxExpression}`); + // Sort the data + const sortedData = [...data].sort((a, b) => a - b); - // Fetch gene expression data for this gene - let geneExpressionData = await cacheGe.fetchWrapperGE(selectedTumorTypes, [gene]); + // Find min and max + const min = sortedData[0]; + const max = sortedData[sortedData.length - 1]; - // Filter by expression range and tumor samples only - let filteredExpressionData = geneExpressionData.filter(record => { - return record.sample_type === "TP" && - record.expression_log2 !== null && - record.expression_log2 !== undefined && - !isNaN(record.expression_log2) && - record.expression_log2 >= minExpression && - record.expression_log2 <= maxExpression; - }); + // If range is too small, use a default bin size + if (max - min < 0.1) return 0.1; - // Extract unique barcodes for this gene - let barcodesForThisGene = filteredExpressionData.map(record => record.tcga_participant_barcode); + // Calculate quartiles + const q1Index = Math.floor(sortedData.length * 0.25); + const q3Index = Math.floor(sortedData.length * 0.75); + const q1 = sortedData[q1Index]; + const q3 = sortedData[q3Index]; + const iqr = q3 - q1; - // Remove duplicates - function onlyUnique(value, index, self) { - return self.indexOf(value) === index; - } - barcodesForThisGene = barcodesForThisGene.filter(onlyUnique); + // Freedman-Diaconis rule: 2 * IQR * n^(-1/3) + const binSize = 2 * iqr * Math.pow(data.length, -1/3); - console.log(`Found ${barcodesForThisGene.length} patients for ${gene} expression filter`); + // If calculated bin size is too small or too large, use reasonable defaults + if (binSize < 0.1 || isNaN(binSize)) return 0.5; + if (binSize > 5) return 2; - // Store barcodes for this gene - histogramFilteredBarcodes.push({ - gene: gene, - range: [minExpression, maxExpression], - barcodes: barcodesForThisGene - }); - } - - // If only one gene filter, return those barcodes - if (histogramFilteredBarcodes.length === 1) { - return histogramFilteredBarcodes[0].barcodes; + return binSize; } - - // If multiple gene filters, compute intersection - if (histogramFilteredBarcodes.length > 1) { - let intersectedBarcodes = histogramFilteredBarcodes[0].barcodes; - - for (let i = 1; i < histogramFilteredBarcodes.length; i++) { - intersectedBarcodes = intersectedBarcodes.filter(barcode => - histogramFilteredBarcodes[i].barcodes.includes(barcode) - ); - } - - console.log(`Intersection of ${histogramFilteredBarcodes.length} expression filters: ${intersectedBarcodes.length} patients`); - return intersectedBarcodes; - } - - return []; -} -/** - * Helper function to calculate appropriate bin size based on data distribution - * - * @param {Array} data - Array of expression values - * @returns {Number} - Appropriate bin size - */ -function calculateBinSize(data) { - // Calculate IQR (Interquartile Range) based bin size using Freedman-Diaconis rule - if (data.length < 2) return 0.5; // Default if not enough data - - // Sort the data - const sortedData = [...data].sort((a, b) => a - b); - - // Find min and max - const min = sortedData[0]; - const max = sortedData[sortedData.length - 1]; - - // If range is too small, use a default bin size - if (max - min < 0.1) return 0.1; - - // Calculate quartiles - const q1Index = Math.floor(sortedData.length * 0.25); - const q3Index = Math.floor(sortedData.length * 0.75); - const q1 = sortedData[q1Index]; - const q3 = sortedData[q3Index]; - const iqr = q3 - q1; - - // Freedman-Diaconis rule: 2 * IQR * n^(-1/3) - const binSize = 2 * iqr * Math.pow(data.length, -1/3); - - // If calculated bin size is too small or too large, use reasonable defaults - if (binSize < 0.1 || isNaN(binSize)) return 0.5; - if (binSize > 5) return 2; - - return binSize; } \ No newline at end of file