+
+
Marker Genes
@@ -585,7 +516,7 @@
-
-
-
-
-
-
-
- Cell Composition
-
-
- Gene Expression
-
-
-
-
-
-
-
- UMAP {{ coordinates.length.toLocaleString() }} cells
-
-
-
-
- Color By
-
-
-
-
-
-
-
-
Cell Count per {{ cellCompositionVars['a'].cellTypeInfo.key }}
-
-
-
-
-
-
-
-
- UMAP {{ geneExpressionVars['a'].selectedGene ? `${geneExpressionVars['a'].selectedGene}` : '' }} {{ coordinates.length.toLocaleString() }} cells
-
-
-
-
-
-
-
-
-
Gene Expression {{ geneExpressionVars['a'].selectedGene ? `${geneExpressionVars['a'].selectedGene}` : '' }}
-
-
-
Display
-
-
-
-
-
-
Subset by
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Marker Genes
-
-
-
-
% Cells Expressing
-
-
-
-
-
-
-
-
-
-
-
@@ -949,7 +607,8 @@
import keyParams from "@/utils/keyParams";
import EventBus from "@/utils/eventBus"
import ResearchUmapPlot from "@/components/researchPortal/ResearchUmapPlot.vue";
- import ResearchBarPlotV2 from "@/components/researchPortal/ResearchBarPlotV2.vue";
+ //import ResearchBarPlotV2 from "@/components/researchPortal/ResearchBarPlotV2.vue"; //DELETE
+ import ResearchStackedBarPlot from "@/components/researchPortal/ResearchStackedBarPlot.vue";
import ResearchDotPlot from "@/components/researchPortal/ResearchDotPlot.vue";
import ResearchViolinPlot from "@/components/researchPortal/ResearchViolinPlot.vue";
import ResearchSingleCellSelector from "@/components/researchPortal/ResearchSingleCellSelector.vue";
@@ -959,7 +618,8 @@
export default Vue.component('research-single-cell-browser', {
components: {
ResearchUmapPlot,
- ResearchBarPlotV2,
+ //ResearchBarPlotV2,
+ ResearchStackedBarPlot,
ResearchDotPlot,
ResearchViolinPlot,
ResearchSingleCellSelector
@@ -1005,6 +665,20 @@
labelColors: null,
colorByOptions: null, //rename?
+ datasetId: null,
+ cellTypeField: null,
+
+ geneNames: [],
+ expressionData: {},
+ expressionStatsAll: [],
+
+ dataLoaded: false,
+ preloadItem: '',
+
+ highlightHoverTimeout: null,
+
+ selectedTabs: {"a":"1", "b":"2"},
+
layout: -1,
cellCompositionVars: {
@@ -1013,17 +687,20 @@
colorByLabel: null,
highlightLabel: '',
highlightLabels: [],
- cellTypeInfo: null
+ segmentByCounts2: null,
+ displayByLabel: null,
+ subsetLabel: "",
},
"b": {
umapColors: null,
colorByLabel: null,
highlightLabel: '',
highlightLabels: [],
- cellTypeInfo: null
+ segmentByCounts2: null,
+ displayByLabel: null,
+ subsetLabel: "",
}
},
-
geneExpressionVars: {
"a": {
umapGeneColors: null,
@@ -1039,25 +716,7 @@
selectedLabel: null,
subsetLabel: "",
}
- },
-
- displayByLabel: '',
- segmentByLabel: '',
- segmentByCounts: null,
-
- datasetId: null,
- cellTypeField: null,
-
- geneNames: [],
- expressionData: {},
- expressionStats: [],
- expressionStats2: [],
-
- dataLoaded: false,
- preloadItem: '',
- highlightHoverTimeout: null,
-
- selectedTabs: {"a":"1", "b":"2"},
+ }
}
},
watch: {
@@ -1069,14 +728,15 @@
Object.keys(this.expressionData).forEach(gene => {
expressionStats.push(...this.getExpressionStats(gene, this.cellTypeField, null, true));
})
- this.expressionStats2 = expressionStats;
- console.log('updated expression stats', this.expressionStats2);
+ this.expressionStatsAll = expressionStats;
+ console.log('updated expression stats', this.expressionStatsAll);
}
},
mounted() {
//load metadata from renderConfig
console.log('renderConfig', this.renderConfig);
console.log('data', this.data);
+
EventBus.$on('on-select',this.handleSelectEvent);
this.init();
},
@@ -1113,21 +773,25 @@
clean(){
this.expressionData = {};
this.geneNames = [];
- this.expressionStats2 = [];
+ this.expressionStatsAll = [];
this.cellCompositionVars = {
"a": {
umapColors: null,
colorByLabel: null,
highlightLabel: '',
highlightLabels: [],
- cellTypeInfo: null
+ segmentByCounts2: null,
+ displayByLabel: null,
+ subsetLabel: "",
},
"b": {
umapColors: null,
colorByLabel: null,
highlightLabel: '',
highlightLabels: [],
- cellTypeInfo: null
+ segmentByCounts2: null,
+ displayByLabel: null,
+ subsetLabel: "",
}
},
this.geneExpressionVars = {
@@ -1218,31 +882,19 @@
this.cellTypeField = this.presetsConfig?.["cell type label"] || Object.keys(this.rawData["metadata_labels"])[0];
console.log("cellTypeField", this.cellTypeField, this.presetsConfig);
-
-
+
+ //preset base visualizers to display by cell type
this.cellCompositionVars['a'].colorByLabel = this.cellTypeField;
this.selectColorBy(this.cellTypeField, 'a');
this.selectColorBy(this.cellTypeField, 'b');
-
- this.updateCellsInfo(this.cellTypeField, 'a');
- this.updateCellsInfo(this.cellTypeField, 'b');
+
+ this.selectSegmentBy(this.cellTypeField, "", 'a');
+ this.selectSegmentBy(this.cellTypeField, "", 'b');
this.geneExpressionVars['a'].selectedLabel = this.cellTypeField;
this.geneExpressionVars['b'].selectedLabel = this.cellTypeField;
-
- if(this.showCellProportion){
- if(this.rawData['metadata_labels']['Diabetes Status']) {
- this.selectSegmentBy(this.cellTypeField, "Diabetes Status");
- }else{
- this.selectSegmentBy(this.cellTypeField, Object.keys(this.rawData["metadata_labels"])[0]);
- }
- }else{
- console.log('cell proportion component disabled')
- }
-
-
//load gene data from parameters
if(this.renderConfig["parameters"]?.gene){
const paramGenes = decodeURIComponent(keyParams[this.renderConfig["parameters"].gene]);
@@ -1309,6 +961,7 @@
}
},
async fetchGeneExpression(gene){
+ //return;
const expressionDataPoint = this.renderConfig["data points"].find(x => x.role === "expression");
const expressionUrl = expressionDataPoint.url.replace('$datasetId', this.datasetId).replace('$gene', gene);
@@ -1339,9 +992,9 @@
//update query string gene params
if(this.renderConfig["parameters"]?.gene){
- const paramGenes = decodeURIComponent(keyParams[this.renderConfig["parameters"].gene]);
+ let paramGenes = decodeURIComponent(keyParams[this.renderConfig["parameters"].gene]);
if(paramGenes){
- const paramGenesArray = paramGenes.toLowerCase().split(',');
+ const paramGenesArray = paramGenes==='undefined' ? [] : paramGenes.toLowerCase().split(',');
console.log(`try adding: ${gene} to ${paramGenesArray}`)
if(!paramGenesArray.includes(gene.toLowerCase())){
paramGenesArray.push(gene);
@@ -1353,13 +1006,6 @@
}
await Vue.nextTick();
-
- //this.expressionStats = this.parseGeneExpression(this.cellTypeField);
- //this.expressionStats2[gene] = this.getExpressionStats(gene, this.cellTypeField, null, true);
- //Vue.set(this.expressionStats2, gene, this.getExpressionStats(gene, this.cellTypeField, null, true));
- //this.expressionStats2 = this.getExpressionStats(gene, this.cellTypeField, null, true);
- //console.log(' stats', this.expressionStats);
- //console.log(' stats2', this.expressionStats2);
if(!this.geneExpressionVars['a'].selectedGene){
this.geneClick(gene, 'a');
@@ -1389,24 +1035,21 @@
console.log('color by:', val);
g.colorByLabel = val;
g.umapColors = this.getColorsByLabel(g.colorByLabel);
- this.updateCellsInfo(g.colorByLabel, group);
},
- selectSegmentBy(display, segment){
+ selectSegmentBy(display, segment, group){
const displayVal = typeof display === 'object' ? display.target.value : display;
const segmentVal = typeof segment === 'object' ? segment.target.value : segment;
- console.log('segment by:', {displayVal, segmentVal});
- this.displayByLabel = displayVal
- this.segmentByLabel = segmentVal;
- //this.segmentByCounts = this.getCountsByLabelSubset(this.labels, this.displayByLabel, this.segmentByLabel);
- this.segmentByCounts = this.getCounts(this.displayByLabel, this.segmentByLabel);
- console.log('segmentByCounts', this.segmentByCounts);
+ const g = this.cellCompositionVars[group];
+ console.log('segment by:', {displayVal, segmentVal, group});
+ g.displayByLabel = displayVal
+ g.segmentByLabel = segmentVal;
+ g.segmentByCounts2 = this.getCounts2(g.displayByLabel, g.segmentByLabel);
+ console.log('segmentByCounts2', g.segmentByCounts2);
},
selectExpressionBy(e, group){
const val = typeof e === 'object' ? e.target.value : e;
const g = this.geneExpressionVars[group];
console.log('expression by:', val, group, g);
- //g.selectedLabel = val;
- //g.expressionStats = this.parseGeneExpression(g.selectedLabel);
g.expressionStats = this.getExpressionStats(g.selectedGene, g.selectedLabel, g.subsetLabel);
},
calcLabelColors(rawData){
@@ -1468,10 +1111,7 @@
geneClick(e, group){
const gene = e;
const g = this.geneExpressionVars[group];
- //g.expressionStats = this.parseGeneExpression(g.selectedLabel);
- //g.subsetLabel = "bmi__group";
g.expressionStats = this.getExpressionStats(gene, g.selectedLabel, g.subsetLabel);
- //console.log(' #####', gene, g)
g.selectedGene = gene;
g.umapGeneColors = this.getUmapExpressionColors(gene);
},
@@ -1504,17 +1144,6 @@
/*
cell composition
*/
- async updateCellsInfo(cellTypeCategory, group){
- const g = this.cellCompositionVars[group];
- g.cellTypeInfo = {
- key: cellTypeCategory,
- data: {[cellTypeCategory]: this.getCounts(cellTypeCategory)},
- colors: Object.values(this.labelColors[cellTypeCategory])
- };
- console.log(" cellTypeInfo", g.cellTypeInfo);
-
- return;
- },
getCellLabels(rawData, labelKeys){
console.log('get CellAnnotations');
@@ -1580,6 +1209,56 @@
console.log(' >>>', result);
return result;
},
+ getCounts2(primaryKey, subsetKey){
+ console.log('getCounts2', primaryKey, subsetKey)
+ const keys = this.rawData["metadata_labels"];
+ const values = this.rawData["metadata"];
+
+ const primaryLabels = keys[primaryKey];
+ const primaryValues = values[primaryKey];
+
+ const result = [];
+
+ if (!subsetKey) {
+ // calculate counts by primary key only
+ primaryLabels.forEach((label, index) => {
+ const indices = primaryValues
+ .map((value, i) => (value === index ? i : -1))
+ .filter(i => i !== -1);
+
+ result.push({
+ [primaryKey]: label,
+ count: indices.length,
+ color: this.labelColors[primaryKey][label]
+ });
+ });
+ } else {
+ // calculate counts grouped by primary key and subset key
+ const subsetValues = values[subsetKey];
+ const subsetLabels = keys[subsetKey];
+
+ primaryLabels.forEach((primaryLabel, primaryIndex) => {
+ const primaryIndices = primaryValues
+ .map((value, i) => (value === primaryIndex ? i : -1))
+ .filter(i => i !== -1);
+
+ subsetLabels.forEach((subsetLabel, subsetIndex) => {
+ const subsetIndices = primaryIndices.filter(
+ i => subsetValues[i] === subsetIndex
+ );
+ result.push({
+ [primaryKey]: primaryLabel,
+ [subsetKey]: subsetLabel,
+ count: subsetIndices.length,
+ color: this.labelColors[subsetKey][subsetLabel]
+ })
+ });
+ });
+ }
+
+ console.log(' >>>', result);
+ return result;
+ },
/*
gene expression
@@ -1592,60 +1271,6 @@
await this.fetchGeneExpression(gene.toUpperCase());
})
},
- parseGeneExpression(category){
- //const categories = side==='left'?this.categoriesLeft:this.categoriesRight;
-
- console.log('parseGeneExpression');
-
- // Get expressinon values for user selected categories
- const expressionByCategory = (category) => {
- const categoryLabels = this.rawData['metadata_labels'][category];//.slice().sort((a, b) => a.localeCompare(b));
- const categoryData = this.rawData['metadata'][category];
- const geneExpression = {};
- const sumstat = {};
-
- //geneNames * 160000 + geneNames * labels
- this.geneNames.forEach(gene => {
-
- if(!geneExpression[gene]) geneExpression[gene] = {}
-
- categoryData.forEach((labelIdx, cellIdx) => {
- const label = categoryLabels[labelIdx];
- if (!geneExpression[gene][label]) {
- geneExpression[gene][label] = [];
- }
- geneExpression[gene][label].push(this.expressionData[gene][cellIdx]);
- });
- geneExpression[gene] = geneExpression[gene];//this.sortObjectKeysLocale(geneExpression[gene]);
-
- categoryLabels.forEach(label => {
- //const sortedValues = geneExpression[gene][label] ? geneExpression[gene][label].sort(d3.descending) : [0];
- const exprValues = geneExpression[gene][label] ? geneExpression[gene][label].sort(d3.ascending) : [0];
- const key = label;
- const mean = d3.mean(exprValues)
- const q1 = d3.quantile(exprValues, .25)
- const median = d3.quantile(exprValues, .5)
- const q3 = d3.quantile(exprValues, .75)
- const interQuantileRange = q3 - q1
- const min = exprValues[0]
- const max = exprValues[exprValues.length-1]
- const pctExpr = (exprValues.filter(val => val > 0).length / exprValues.length) * 100;//sortedValues.length / 166149;
- if(!sumstat[gene]) sumstat[gene] = [];
- sumstat[gene].push({ key, mean, q1, median, q3, interQuantileRange, min, max, pctExpr, exprValues });
-
- })
- })
-
- return sumstat;
- }
-
- const e = expressionByCategory(category);
-
- console.log(' expressionByCategory', e);
-
- return [e];
- },
-
getExpressionStats(gene, primaryKey, subsetKey, partial=false) {
const expression = this.expressionData[gene];
const keys = this.rawData["metadata_labels"];
@@ -1681,15 +1306,11 @@
.map((value, i) => (value === primaryIndex ? i : -1))
.filter(i => i !== -1);
- //result[primaryLabel] = {};
-
subsetLabels.forEach((subsetLabel, subsetIndex) => {
const subsetIndices = primaryIndices.filter(
i => subsetValues[i] === subsetIndex
);
-
const exprValues = subsetIndices.map(i => expression[i]);
- //result[primaryLabel][subsetLabel] = this.calculateStats(exprValues);
result.push({
gene: gene,
[primaryKey]: primaryLabel,
@@ -1739,7 +1360,7 @@
const geneData = this.expressionData[gene];
//console.log('---', this.expressionData, this.geneNames, this.expressionData[gene])
const color = d3.scaleSequential(d3.interpolatePlasma)
- .domain([d3.max(geneData), 0]);
+ .domain([d3.max(geneData), 0]);
for(var i=0; i
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/researchPortal/ResearchViolinPlot.vue b/src/components/researchPortal/ResearchViolinPlot.vue
index 43610124d..0c520e81d 100644
--- a/src/components/researchPortal/ResearchViolinPlot.vue
+++ b/src/components/researchPortal/ResearchViolinPlot.vue
@@ -49,6 +49,8 @@
},
data() {
return {
+ eventElements: [],
+ tooltip: null,
}
},
watch: {
@@ -69,6 +71,9 @@
},
beforeDestroy(){
//window.removeEventListener('resize', this.handleResize);
+ if(this.eventElements.length>0) {
+ this.removeAllListeners(this.eventElements);
+ }
},
methods: {
handleResize(){
@@ -80,13 +85,17 @@
if(!this.data) return;
- const tooltip = this.$refs.tooltip;
+ //clear previous event listeners
+ if(this.eventElements.length>0) {
+ this.removeAllListeners(this.eventElements);
+ this.eventElements = [];
+ }
+
+ const tooltip = this.tooltip = this.$refs.tooltip;
const primaryKey = this.primaryKey;
const subsetKey = this.subsetKey;
-
+ const hasSubsetKey = subsetKey;
const keys = Array.from(new Set(this.data.map((d) => d[primaryKey])));
- const hasSubsetKey = subsetKey && this.data[0][subsetKey] && this.data[0][subsetKey] !== "";
- const domain = hasSubsetKey ? this.data.map((d) => d[primaryKey] +' - '+d[subsetKey]) : keys;
//pre-render x-axis labels to get the their max height
//this way we can ensure long labels dont get cut off at the bottom
@@ -108,7 +117,7 @@
//calculate sizes and margins
const parentWidth = this.$refs.chartWrapper.parentElement.offsetWidth;
const labels = { xAxis: this.xAxisLabel?20:0, yAxis: this.yAxisLabel?20:0 }
- const margin = { top: 20, right: 10, bottom: labelsHeight + labels.xAxis, left: 40 };
+ const margin = { top: 10, right: 10, bottom: labelsHeight + labels.xAxis, left: 40 };
let width = parentWidth;
let height = this.height;
if(margin.bottom > (height/2)){
@@ -127,6 +136,7 @@
width = plotWidth + margin.left + margin.right;
*/
+ //get absolute min/max values
const min = d3.min(this.data, (d) => d.min);
const max = d3.max(this.data, (d) => d.max);
@@ -135,6 +145,7 @@
.attr('width', width)
.attr('height', height)
+ //rednder axis labels
if(this.xAxisLabel){
const label = svg.append('g')
.append('text')
@@ -155,7 +166,8 @@
}
const plot = svg.append("g")
- .attr("transform", `translate(${margin.left+labels.xAxis},${margin.top})`);
+ .attr("transform", `translate(${margin.left+labels.xAxis},${margin.top})`)
+ .attr("class", 'plot');
const entryKey = (entry) => {
if(hasSubsetKey){
@@ -165,6 +177,8 @@
}
}
+ const domain = hasSubsetKey ? this.data.map((d) => d[primaryKey] +' - '+d[subsetKey]) : keys;
+
// x scale
const x = d3.scaleBand()
.domain(domain)
@@ -200,6 +214,8 @@
const boxWidth = x.bandwidth() * 0.6;
+ //add background boxes to separate primaryKey sections
+ //when they have subsetKey items
if(hasSubsetKey){
keys.forEach((key, i) => {
plot.append('rect')
@@ -211,15 +227,19 @@
})
}
+ //draw the violins
this.data.forEach((entry) => {
const xCenter = x(entryKey(entry)) + x.bandwidth() / 2;
const box = plot.append('g')
.attr("width", boxWidth)
- .attr("class", "violin-group")
- .attr("data-key", entryKey(entry));
+ .attr('class', 'bar')
+ .attr('data-label', `${entry[primaryKey]},${hasSubsetKey ? entry[subsetKey] : ''}`)
+ /*.attr("class", "violin-group")
+ .attr("data-key", entryKey(entry));*/
const boxNode = box.node();
+ this.addListener(boxNode, entry);
// kde
const bandwidth = 1;
@@ -300,33 +320,6 @@
.attr("height", y(entry.min) - y(entry.max))
.attr("fill", "transparent")
.style("pointer-events", "all");
-
-
- // Tooltip mouseover
- boxNode.addEventListener('mouseover', function(e){
- tooltip.innerHTML = `
- ${primaryKey}:
${entry[primaryKey]}
- ${subsetKey}:
${entry[subsetKey]}
-
-
-
- Median:
${entry.median.toFixed(4)}
-
-
- `;
- tooltip.classList.add('show')
- })
- // Tooltip mousemove to follow the cursor
- boxNode.addEventListener('mousemove', function(e){
- tooltip.style.top = (e.clientY - 10) + "px";
- tooltip.style.left = (e.clientX + 10) + "px";
- })
- // Tooltip mouseout to hide it
- boxNode.addEventListener('mouseout', function(e){
- tooltip.classList.remove('show');
- tooltip.style.top = -1000 + "px";
- tooltip.style.left = -1000 + "px";
- });
});
},
kde(kernel, thresholds, data) {
@@ -338,16 +331,72 @@
return Math.abs(u) <= 1 ? 0.75 * (1 - u * u) / bandwidth : 0;
};
},
- doHighlight(key){
- const svg = this.$refs.chart;
- const violins = svg.querySelectorAll('.violin-group');
- violins.forEach(violin=>{
- if(!key || violin.dataset.key===key){
- violin.style.opacity = '1';
- }else{
- violin.style.opacity = '0.1';
- }
+ addListener(el, entry){
+ const mouseOver = this.mouseOverHandler.bind(this, entry);
+ const mouseMove = this.mouseMoveHandler.bind(this);
+ const mouseOut = this.mouseOutHandler.bind(this);
+ el._listeners = { mouseOver, mouseMove, mouseOut };
+ this.eventElements.push(el);
+ // Tooltip mouseover
+ el.addEventListener('mouseover', mouseOver);
+ el.addEventListener('mousemove', mouseMove);
+ el.addEventListener('mouseout', mouseOut);
+ },
+ removeListener(el){
+ if(el._listeners){
+ el.addEventListener('mouseover', el._listeners.mouseOver);
+ el.addEventListener('mousemove', el._listeners.mouseMove);
+ el.addEventListener('mouseout', el._listeners.mouseOut);
+ delete el._listeners;
+ }
+ },
+ removeAllListeners(elsArr){
+ console.log(`removing event listeners for ${elsArr.length} elements`);
+ elsArr.forEach(el=>{
+ this.removeListener(el);
+ });
+ },
+ mouseOverHandler(entry){
+ this.tooltip.innerHTML = `${this.primaryKey}:
${entry[this.primaryKey]}
+ ${this.subsetKey}:
${entry[this.subsetKey]}
+
+
+
+ Median:
${entry.median.toFixed(4)}
+
+
+ `;
+ this.tooltip.classList.add('show')
+ },
+ mouseMoveHandler(e){
+ this.tooltip.style.top = (e.clientY - 10) + "px";
+ this.tooltip.style.left = (e.clientX + 10) + "px";
+ },
+ mouseOutHandler(e){
+ this.tooltip.classList.remove('show');
+ this.tooltip.style.top = -1000 + "px";
+ this.tooltip.style.left = -1000 + "px";
+ },
+ doHighlight(label){
+ const plot = this.$refs.chart.querySelector(`.plot`);
+ if(!plot) return;
+ plot.classList.remove('highlighting');
+ const matchingEls = this.$refs.chart.querySelectorAll(`.plot .bar`);
+ matchingEls.forEach(el => {
+ el.classList.remove('on');
})
+ if(label && label != ''){
+ const matchingEls = this.$refs.chart.querySelectorAll(`.bar[data-label*="${label}"]`);
+ let hasHighlight = false;
+ matchingEls.forEach(el => {
+ const elLabels = el.dataset.label.split(',');
+ if(elLabels.includes(label)){
+ el.classList.add('on');
+ hasHighlight=true;
+ }
+ })
+ if(hasHighlight) plot.classList.add('highlighting');
+ }
}
},
});
@@ -370,5 +419,11 @@
.tooltip.show{
opacity: 1;
}
+ ::v-deep .plot.highlighting .bar{
+ opacity: 0.2;
+ }
+ ::v-deep .plot.highlighting .bar.on{
+ opacity: 1;
+ }
\ No newline at end of file
diff --git a/src/components/researchPortal/customComponents/cfdeLanding.vue b/src/components/researchPortal/customComponents/cfdeLanding.vue
index 24d72dd7c..b2e094f29 100644
--- a/src/components/researchPortal/customComponents/cfdeLanding.vue
+++ b/src/components/researchPortal/customComponents/cfdeLanding.vue
@@ -386,7 +386,7 @@