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