From f5a11b0d4e59f168d31a8a0e86be0d834615d458 Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Wed, 30 Oct 2024 09:17:35 -0400 Subject: [PATCH 01/10] starting component --- .../researchPortal/ResearchSection.vue | 24 +- .../ResearchSectionVisualizers.vue | 11 +- .../ResearchSingleCellBrowser.vue | 254 ++++++++++ .../researchPortal/ResearchUmapPlot.vue | 462 ++++++++++++++++++ src/utils/dataConvert.js | 19 + 5 files changed, 763 insertions(+), 7 deletions(-) create mode 100644 src/components/researchPortal/ResearchSingleCellBrowser.vue create mode 100644 src/components/researchPortal/ResearchUmapPlot.vue diff --git a/src/components/researchPortal/ResearchSection.vue b/src/components/researchPortal/ResearchSection.vue index 1acdcbfc7..4ec1c61c7 100644 --- a/src/components/researchPortal/ResearchSection.vue +++ b/src/components/researchPortal/ResearchSection.vue @@ -334,7 +334,6 @@ export default Vue.component("research-section", { this.openInfoCard = this.utils.keyParams[infoCardConfig['key']]; } } - if (!!this.sectionConfig["data point"] && !!this.sectionConfig["data point"]["parameters point"]) { let listPoint = this.sectionConfig["data point"]["parameters point"]; this.getList( @@ -899,8 +898,9 @@ export default Vue.component("research-section", { queryData(FROM) { let queryType = this.dataPoint["type"]; - let paramsType = this.dataPoint["parameters type"] - let params = this.dataPoint["parameters"] + let paramsType = this.dataPoint["parameters type"]; + let params = this.dataPoint["parameters"]; + let dataType = this.dataPoint["data type"] // if data isn't getting cumulated, remove older search params other than the last one if (!this.dataPoint["cumulate data"] && this.searched.length > 1) { let lastSearched = this.searched[this.searched.length - 1] @@ -920,7 +920,7 @@ export default Vue.component("research-section", { this.queryBioindex(paramsString, paramsType, params); break; case "api": - this.queryApi(paramsString, paramsType, params); + this.queryApi(paramsString, paramsType, params, dataType); break; case "file": let parameter = this.dataPoint["parameter"] @@ -1065,7 +1065,7 @@ export default Vue.component("research-section", { } }, - async queryApi(QUERY, TYPE, PARAMS) { + async queryApi(QUERY, TYPE, PARAMS, DATATYPE) { if (QUERY != "") { this.searched.push(QUERY); @@ -1103,7 +1103,14 @@ export default Vue.component("research-section", { }) } - let contentJson = await fetch(dataUrl).then((resp) => resp.json()); + let contentJson; + if(DATATYPE && DATATYPE === "line json"){ + const response = await fetch(dataUrl).then(resp => resp.text()); + const lines = response.split('\n').filter(line => line.trim() !== ''); + contentJson = lines.map(line => JSON.parse(line)); + }else{ + contentJson = await fetch(dataUrl).then((resp) => resp.json()); + } if (contentJson.error == null) { @@ -1345,6 +1352,11 @@ export default Vue.component("research-section", { break; + case "line json": + data = CONTENT; + + break; + case "csv": if (!!dataWrapper) { diff --git a/src/components/researchPortal/ResearchSectionVisualizers.vue b/src/components/researchPortal/ResearchSectionVisualizers.vue index a321da594..c15228b07 100644 --- a/src/components/researchPortal/ResearchSectionVisualizers.vue +++ b/src/components/researchPortal/ResearchSectionVisualizers.vue @@ -167,6 +167,13 @@ :starItems="starItems" :utils="utils" > + + @@ -190,6 +197,7 @@ import ResearchBarPlot from "@/components/researchPortal/ResearchBarPlot.vue"; import ResearchBoxPlot from "@/components/researchPortal/ResearchBoxPlot.vue"; import ResearchRegionTrack from "@/components/researchPortal/ResearchRegionTrack.vue"; import ResearchRegionDotsTrack from "@/components/researchPortal/ResearchRegionDotsTrack.vue"; +import ResearchSingleCellBrowser from "@/components/researchPortal/ResearchSingleCellBrowser.vue"; export default Vue.component("research-section-visualizers", { props: ["plotConfig","plotData","plotLegend","phenotypeMap","plotMargin","colors", @@ -209,7 +217,8 @@ export default Vue.component("research-section-visualizers", { ResearchBarPlot, ResearchBoxPlot, ResearchRegionTrack, - ResearchRegionDotsTrack + ResearchRegionDotsTrack, + ResearchSingleCellBrowser }, data() { return { diff --git a/src/components/researchPortal/ResearchSingleCellBrowser.vue b/src/components/researchPortal/ResearchSingleCellBrowser.vue new file mode 100644 index 000000000..560d6e084 --- /dev/null +++ b/src/components/researchPortal/ResearchSingleCellBrowser.vue @@ -0,0 +1,254 @@ + + + + + + \ No newline at end of file diff --git a/src/components/researchPortal/ResearchUmapPlot.vue b/src/components/researchPortal/ResearchUmapPlot.vue new file mode 100644 index 000000000..7b156e7f2 --- /dev/null +++ b/src/components/researchPortal/ResearchUmapPlot.vue @@ -0,0 +1,462 @@ + + + + + + \ No newline at end of file diff --git a/src/utils/dataConvert.js b/src/utils/dataConvert.js index 2cd71ac24..84bc62118 100644 --- a/src/utils/dataConvert.js +++ b/src/utils/dataConvert.js @@ -374,6 +374,24 @@ let csv2Json = function (DATA) { return jsonData; }; +let tsv2Json = function (DATA) { + const lines = DATA.split('\n'); + const headers = lines.shift().split('\t'); + const jsonArray = []; + const ifNumber = (str) => { + return !isNaN(str) && str.trim() !== '' ? Number(str) : str; + } + lines.forEach(line => { + const values = line.split('\t'); + const obj = {}; + headers.forEach((header, index) => { + obj[header] = ifNumber(values[index]); + }); + jsonArray.push(obj); + }); + return jsonArray; +} + let flatJson = function (DATA) { /// first wrap values with comma @@ -550,6 +568,7 @@ const object2Array = function (DATASET, COMPARECONFIG, KEY) { export default { convertData, csv2Json, + tsv2Json, object2Array, flatJson }; From 7cc4e322687fbe9c061a7675b45392d0a0eac21d Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Mon, 4 Nov 2024 11:10:09 -0500 Subject: [PATCH 02/10] further buildout of components --- .../researchPortal/ResearchBarPlotV2.vue | 650 ++++++++++++++++++ .../researchPortal/ResearchDotPlot.vue | 411 +++++++++++ .../ResearchSingleCellBrowser.vue | 467 +++++++++++-- .../researchPortal/ResearchUmapPlot.vue | 77 ++- 4 files changed, 1527 insertions(+), 78 deletions(-) create mode 100644 src/components/researchPortal/ResearchBarPlotV2.vue create mode 100644 src/components/researchPortal/ResearchDotPlot.vue diff --git a/src/components/researchPortal/ResearchBarPlotV2.vue b/src/components/researchPortal/ResearchBarPlotV2.vue new file mode 100644 index 000000000..0af74bbbb --- /dev/null +++ b/src/components/researchPortal/ResearchBarPlotV2.vue @@ -0,0 +1,650 @@ + + + + + + \ No newline at end of file diff --git a/src/components/researchPortal/ResearchDotPlot.vue b/src/components/researchPortal/ResearchDotPlot.vue new file mode 100644 index 000000000..b69e3c385 --- /dev/null +++ b/src/components/researchPortal/ResearchDotPlot.vue @@ -0,0 +1,411 @@ + + + + + \ No newline at end of file diff --git a/src/components/researchPortal/ResearchSingleCellBrowser.vue b/src/components/researchPortal/ResearchSingleCellBrowser.vue index 560d6e084..c5754a426 100644 --- a/src/components/researchPortal/ResearchSingleCellBrowser.vue +++ b/src/components/researchPortal/ResearchSingleCellBrowser.vue @@ -5,41 +5,159 @@ > Please Select a Tissue - -
-
- +
+
+
+
+
Title
+
{{ this.data[0]["Name"] }}
+
+
+
Authors
+
{{ this.data[0]["Authors"] || 'N/A' }}
+
+
+
Summary
+
{{ this.data[0]["Summary"] }}
+
+
+
+
+
Species
+
{{ this.data[0]["Species"] }}
+
+
+
Tissue
+
{{ this.data[0]["Tissue"] }}
+
+
+
+
+
Source
+
{{ this.data[0]["Source"] }}
+
+
+
Method
+
{{ this.data[0]["Method"] }}
+
+
+
Platform
+
{{ this.data[0]["Platform"] }}
+
+
-
-
Color By
- -
-
-
{{ label }}
+
+
+ {{ this.data?.length === 1 ? this.data[0]["Tissue"] : "" }} +
+ anatomogram +
+
+
+
+ UMAP {{ coordinates.length.toLocaleString() }} cells +
+ +
+
+ Annotations +
+
+ +
+ +
+ +
+
+
+ +
+ +
{{ label }}
+
+
+
+
+ Gene Search +
+
+ +
+ + +
+
+
+
+
+ +
+
{{ gene }}
+
+
+
+
+ Cell Composition + +
+
+ Gene Expression + +
+
@@ -48,12 +166,16 @@ import * as d3 from 'd3'; import Vue from 'vue'; import ResearchUmapPlot from "@/components/researchPortal/ResearchUmapPlot.vue"; + import ResearchBarPlotV2 from "@/components/researchPortal/ResearchBarPlotV2.vue"; + import ResearchDotPlot from "@/components/researchPortal/ResearchDotPlot.vue"; const colors = ["#007bff","#048845","#8490C8","#BF61A5","#EE3124","#FCD700","#5555FF","#7aaa1c","#F88084","#9F78AC","#F5A4C7","#CEE6C1","#cccc00","#6FC7B6","#D5A768","#d4d4d4"] export default Vue.component('research-single-cell-browser', { components: { - ResearchUmapPlot + ResearchUmapPlot, + ResearchBarPlotV2, + ResearchDotPlot }, props: { renderConfig: { @@ -82,15 +204,20 @@ colorByLabel: null, colorByOptions: null, - umapSize: 500, umapColors: null, datasetName: null, cellTypeField: null, + cellTypeInfo: null, + + geneNames: [], + expressionData: {}, + expressionStats: [], preloadItem: '', tissueDisplay: '', - highlightLabel: '' + highlightLabel: '', + highlightHoverTimeout: null, } }, watch: { @@ -135,6 +262,7 @@ } console.log(`loading dataset: ${this.data[0].datasetId}`); + console.log(' data', this.data[0]); this.tissueDisplay = this.data[0]["Tissue"]; this.datasetName = this.data[0]["Name"]; @@ -149,9 +277,19 @@ //pre-calculate colors for fields in each category this.labelColors = this.calcLabelColors(this.rawData); - this.colorScalePlasmaColorsArray = d3.range(0, 1.01, 0.1).map(t => this.colorScalePlasma(t)).join(', ') + this.colorScalePlasmaColorsArray = d3.range(0, 1.01, 0.1).map(t => this.colorScalePlasma(t)).join(', '); + + this.expressionStats = []; this.getColorByOptions(); + + this.updateCellsInfo(this.cellTypeField); + + if(this.renderConfig["data fields"]?.["genes"]){ + this.renderConfig["data fields"]["genes"].forEach(async (gene) => { + await this.fetchGeneExpression(gene.toUpperCase()); + }) + } }, getColorByOptions(){ const dataFields = this.renderConfig["data fields"]; @@ -168,16 +306,18 @@ this.colorByOptions = colorByOptions; this.selectColorBy(this.cellTypeField); + }, updateDataUrl(url, paramaters){ let updatedUrl = url; //tmp - updatedUrl = updatedUrl.replace(`$datasetId`, this.data[0]['Tissue'].toLowerCase()); + //updatedUrl = updatedUrl.replace(`$datasetId`, this.data[0]['Tissue'].toLowerCase()); //use below once metadata is fixed - /*paramaters.forEach(param => { - updatedUrl = updatedUrl.replace(`$${param}`, this.data[0][param]); - })*/ + paramaters.forEach(param => { + if(param !== 'gene') + updatedUrl = updatedUrl.replace(`$${param}`, this.data[0][param].toLowerCase().replaceAll(" ", "_")); + }) return updatedUrl; }, async fetchFields() { @@ -196,8 +336,8 @@ }, async fetchCoordinates() { console.log('getting coordinates'); - const fieldsDataPoint = this.renderConfig["data points"].find(x => x.role === "coordinates"); - const coordinatesUrl = this.updateDataUrl(fieldsDataPoint.url, fieldsDataPoint.parameters); + const coordinatesDataPoint = this.renderConfig["data points"].find(x => x.role === "coordinates"); + const coordinatesUrl = this.updateDataUrl(coordinatesDataPoint.url, coordinatesDataPoint.parameters); try { const response = await fetch(coordinatesUrl); const json = this.utils.dataConvert.tsv2Json(await response.text()); @@ -207,11 +347,45 @@ console.error('Error fetching coordinates:', error); } }, + async fetchGeneExpression(gene){ + console.log('fetchGeneExpression', gene); + const expressionDataPoint = this.renderConfig["data points"].find(x => x.role === "expression"); + const expressionUrl = this.updateDataUrl(expressionDataPoint.url, expressionDataPoint.parameters); + //this.isLoading = true; + await Vue.nextTick(); + try{ + const response = await fetch(expressionUrl+','+gene); + const json = await response.json(); + const expression = json.data[0]['expression']; + this.geneNames.push(gene); + this.expressionData[gene] = expression; + console.log(' ', expression); + + //this.isLoading = false; + await Vue.nextTick(); + + //this.parseGeneExpression(); + if(this.datasetSettings){ + //const geneStats = await this.getGeneExpression(this.expressionData[gene], this.datasetSettings.selectedCellType); + //this.expressionStats.push({[gene]:geneStats}); + }else{ + this.expressionStats = this.parseGeneExpression(this.cellTypeField); + //this.expressionStats.push(geneStats); + console.log('expressionStats', this.expressionStats); + } + if(!this.activeGene || this.activeGene===''){ + this.activeGene = gene; + } + }catch(error){ + console.error(' Error fetching gene expression', error); + } + }, selectColorBy(e){ const val = typeof e === 'object' ? e.target.value : e; console.log(val); this.colorByLabel = val; this.umapColors = this.getColorsByLabel(this.colorByLabel); + this.updateCellsInfo(this.colorByLabel); }, calcLabelColors(rawData){ const colors = {}; @@ -222,7 +396,7 @@ this.colorIndex++; } } - //console.log('labelColors', colors); + console.log('labelColors', colors); return colors; }, getColorsByLabel(category, subset=null){ @@ -238,8 +412,163 @@ return pointColors; }, labelHover(e){ + clearTimeout(this.highlightHoverTimeout); const label = e.target.dataset.label; this.highlightLabel = label; + }, + labelHoverOut(e){ + this.highlightHoverTimeout = setTimeout(() => { + this.highlightLabel = ''; + }, 50); + + }, + + /* + cell composition + */ + async updateCellsInfo(cellTypeCategory, conditionA, conditionB){ + //cell types + const parsedData = this.parseRawDataByCategory(this.rawData, [cellTypeCategory, conditionA, conditionB]); + const counts = this.getCounts(parsedData, [cellTypeCategory]); + this.cellTypeInfo = { + key: cellTypeCategory, + data: counts, + colors: Object.values(this.labelColors[cellTypeCategory]) + }; + console.log("cellTypeInfo", this.cellTypeInfo); + + return; + }, + parseRawDataByCategory(rawData, categoryKeys){ + console.log('parseRawDataByCategory', categoryKeys); + + const { NAME, metadata, metadata_labels } = rawData; + const parsedData = []; + + //process data based on user selected categories + for (let i = 0; i < NAME.length; i++) { + const record = {}; + for (const n in categoryKeys) { + const category = categoryKeys[n]; + if (metadata.hasOwnProperty(category)) { + const labelIdx = metadata[category][i]; + const label = metadata_labels[category][labelIdx]; + record[category] = label; + } + } + parsedData.push(record); + } + + //console.log(' parsedData', parsedData); + + return parsedData; + }, + getCounts(parsedData, categoryKeys, uniqueKey=null){ + console.log('getCounts', parsedData, categoryKeys, uniqueKey); + //get counts from parsed data by keys + const calculateCounts = (data, keys) => { + return keys.reduce((acc, key) => { + console.log(' key', key); + acc[key] = data.reduce((acc, row) => { + acc[row[key]] = (acc[row[key]] || 0) + 1; + return acc; + }, {}); + return acc; + }, {}); + }; + const counts = calculateCounts(parsedData, categoryKeys); + console.log(' total counts', counts); + return counts; + }, + + /* + gene expression + */ + searchGene(e){ + const parts = e.target.value.split(/[,\s]+/); + e.target.value = ''; + //TODO: should be a queue + parts.forEach(async (gene) => { + 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 sortedValues = geneExpression[gene][label] ? geneExpression[gene][label] : [0]; + const key = label; + const mean = d3.mean(sortedValues) + const q1 = d3.quantile(sortedValues, .25) + const median = d3.quantile(sortedValues, .5) + const q3 = d3.quantile(sortedValues, .75) + const interQuantileRange = q3 - q1 + const min = sortedValues[0] + const max = sortedValues[sortedValues.length-1] + const pctExpr = (sortedValues.filter(val => val > 0).length / sortedValues.length) * 100;//sortedValues.length / 166149; + if(!sumstat[gene]) sumstat[gene] = []; + sumstat[gene].push({ key, mean, q1, median, q3, interQuantileRange, min, max, pctExpr }); + + }) + }) + + return sumstat; + } + + const e = expressionByCategory(category); + + console.log('eeeeeeee', e) + + return [e]; + + /* + //set individual category expression data + if(this.categoriesLeft.length===1){ + this.geneExpressionA = expressionByCategory(this.categoriesLeft); + //const sumStatsA = sumstatsByCategory(this.categoriesLeft); + //console.log('sumStatsA', sumStatsA) + }else{ + this.geneExpressionA = null; + } + if(this.categoriesRight.length===1){ + this.geneExpressionB = expressionByCategory(this.categoriesRight); + //const sumStatsB = sumstatsByCategory(this.categoriesRight); + //console.log('sumStatsA', sumStatsB) + }else{ + this.geneExpressionB = null; + } + + console.log(' A, B', this.geneExpressionA, this.geneExpressionB); + + + if(this.activeGene===''){ + this.activeGene = this.geneNames[0]; + } + */ } }, }); @@ -250,5 +579,59 @@ select { background: white; font-size: 14px; } +button { + border: 1px solid rgba(0, 0, 0, .25); + background: white; + color: #4e4e4e; + padding: 1px 3px; + font-size: 14px !important; +} +button:hover { + border: 1px solid rgba(0, 0, 0, .5); +} +.colorize-option{ + cursor:pointer; + display: flex; + align-items: center; + justify-content: center; + svg{ + width:14px; + } + path{ + /*fill:transparent;*/ + opacity: .25; + /*stroke:#434343;*/ + } +} +.colorize-option.active{ + path{ + /*fill:#434343;*/ + opacity: 1; + } +} +.summary-grid{ + display: grid; + grid-template-columns: 620px repeat(2, 1fr); + grid-template-rows: 1fr; + grid-column-gap: 20px; + grid-row-gap: 0px; +} +.summary-title{ + font-weight: bold; +} +.basics-grid{ + display: grid; + grid-template-columns: 200px 400px repeat(2, 1fr); + grid-template-rows: 1fr; + grid-column-gap: 20px; + grid-row-gap: 0px; +} +.counts-grid{ + display: grid; + grid-template-columns: repeat(2, 620px); + grid-template-rows: 1fr; + grid-column-gap: 20px; + grid-row-gap: 0px; +} \ No newline at end of file diff --git a/src/components/researchPortal/ResearchUmapPlot.vue b/src/components/researchPortal/ResearchUmapPlot.vue index 7b156e7f2..390e8a4ee 100644 --- a/src/components/researchPortal/ResearchUmapPlot.vue +++ b/src/components/researchPortal/ResearchUmapPlot.vue @@ -1,12 +1,13 @@ @@ -32,53 +32,81 @@ + + + \ No newline at end of file From 7cc030b385a95665c3213a2d26d67723da21f0ed Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Mon, 2 Dec 2024 12:28:56 -0500 Subject: [PATCH 05/10] adding hover labels to umap buttons and 'colo by' labels --- .../researchPortal/ResearchSingleCellBrowser.vue | 8 +++++++- src/components/researchPortal/ResearchUmapPlot.vue | 14 +++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/researchPortal/ResearchSingleCellBrowser.vue b/src/components/researchPortal/ResearchSingleCellBrowser.vue index e256502cb..8ee364914 100644 --- a/src/components/researchPortal/ResearchSingleCellBrowser.vue +++ b/src/components/researchPortal/ResearchSingleCellBrowser.vue @@ -75,6 +75,7 @@
@@ -95,6 +96,7 @@
@@ -162,11 +164,12 @@
-
+
@@ -247,6 +250,7 @@
@@ -267,6 +271,7 @@
@@ -339,6 +344,7 @@
diff --git a/src/components/researchPortal/ResearchUmapPlot.vue b/src/components/researchPortal/ResearchUmapPlot.vue index 9c2b91f72..638acf50e 100644 --- a/src/components/researchPortal/ResearchUmapPlot.vue +++ b/src/components/researchPortal/ResearchUmapPlot.vue @@ -3,10 +3,10 @@ {{ title }}
- -
@@ -24,7 +24,7 @@ @mouseleave="endPan" > -
+
@@ -560,15 +560,15 @@ .umapTooltip.hidden{ display:none; } - .tooltip{ +.scb-tooltip{ position:fixed; background: white; padding: 5px 10px; box-shadow: rgba(0, 0, 0, 0.5) -4px 9px 25px -6px; - } - .tooltip.show{ +} +.scb-tooltip.show{ opacity: 1; - } +} button { border: 1px solid rgba(0, 0, 0, .25); From f02567c6e3d0a3afd137bfd0f5b74df0e1199fa9 Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Tue, 3 Dec 2024 10:22:48 -0500 Subject: [PATCH 06/10] add umap tooltip for zoom/pan --- src/components/researchPortal/ResearchUmapPlot.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/researchPortal/ResearchUmapPlot.vue b/src/components/researchPortal/ResearchUmapPlot.vue index 638acf50e..6c298fbbc 100644 --- a/src/components/researchPortal/ResearchUmapPlot.vue +++ b/src/components/researchPortal/ResearchUmapPlot.vue @@ -9,6 +9,9 @@ +
@@ -221,7 +224,7 @@ this.pointBoundsCalculated = true; } - if (!this.clusterCentersInitialized && this.fields) { + if (this.fields && !this.clusterCentersInitialized) { this.clusterCenters = {}; points.forEach((coord, index) => { From d2e209b73123929a957208f823a8c7f043fc720c Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Tue, 3 Dec 2024 10:23:33 -0500 Subject: [PATCH 07/10] data cleanup fix --- .../ResearchSingleCellBrowser.vue | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/components/researchPortal/ResearchSingleCellBrowser.vue b/src/components/researchPortal/ResearchSingleCellBrowser.vue index 8ee364914..83d771708 100644 --- a/src/components/researchPortal/ResearchSingleCellBrowser.vue +++ b/src/components/researchPortal/ResearchSingleCellBrowser.vue @@ -32,6 +32,7 @@
+
vs
+
-
+
+
Cell Proportion {{ segmentByLabel }} per {{ displayByLabel }}
@@ -448,6 +451,7 @@ />
+
@@ -646,6 +650,38 @@ this.init(); } }, + clean(){ + this.cellCompositionVars = { + "a": { + umapColors: null, + colorByLabel: null, + highlightLabel: '', + highlightLabels: [], + cellTypeInfo: null + }, + "b": { + umapColors: null, + colorByLabel: null, + highlightLabel: '', + highlightLabels: [], + cellTypeInfo: null + } + }, + this.geneExpressionVars = { + "a": { + umapGeneColors: null, + selectedGene: null, + expressionStats: [], + selectedLabel: null, + }, + "b": { + umapGeneColors: null, + selectedGene: null, + expressionStats: [], + selectedLabel: null, + } + } + }, async init(){ /* if(this.data.length !== 1){ @@ -677,6 +713,8 @@ return; } } + + this.clean(); console.log(`loading dataset: ${this.datasetData.datasetId}`); console.log(' data', this.datasetData); From fdaa58a85d628db51faafd09a0b03c992eb7a372 Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Wed, 4 Dec 2024 14:34:44 -0500 Subject: [PATCH 08/10] starting select component for single cell browser color picker --- .../ResearchSingleCellBrowser.vue | 40 ++- .../ResearchSingleCellSelector.vue | 300 ++++++++++++++++++ 2 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 src/components/researchPortal/ResearchSingleCellSelector.vue diff --git a/src/components/researchPortal/ResearchSingleCellBrowser.vue b/src/components/researchPortal/ResearchSingleCellBrowser.vue index 83d771708..853be2f93 100644 --- a/src/components/researchPortal/ResearchSingleCellBrowser.vue +++ b/src/components/researchPortal/ResearchSingleCellBrowser.vue @@ -69,8 +69,18 @@ :isLoading="isLoadingData" />
-
+
Color By + + +
@@ -110,6 +119,7 @@
+ -->
@@ -247,6 +257,15 @@
Color By + +
@@ -286,6 +304,7 @@
+ -->
@@ -508,6 +527,7 @@ import ResearchBarPlotV2 from "@/components/researchPortal/ResearchBarPlotV2.vue"; import ResearchDotPlot from "@/components/researchPortal/ResearchDotPlot.vue"; import ResearchViolinPlot from "@/components/researchPortal/ResearchViolinPlot.vue"; + import ResearchSingleCellSelector from "@/components/researchPortal/ResearchSingleCellSelector.vue"; const colors = ["#007bff","#048845","#8490C8","#BF61A5","#EE3124","#FCD700","#5555FF","#7aaa1c","#F88084","#9F78AC","#F5A4C7","#CEE6C1","#cccc00","#6FC7B6","#D5A768","#d4d4d4"] @@ -516,7 +536,8 @@ ResearchUmapPlot, ResearchBarPlotV2, ResearchDotPlot, - ResearchViolinPlot + ResearchViolinPlot, + ResearchSingleCellSelector }, props: { sectionId: { @@ -970,6 +991,17 @@ return false; }, + handleSelectorUpdate(e, group, id){ + console.log('selector updated', group, id, e); + this.cellCompositionVars[group].highlightLabels = e.coloredLabels; + this.selectColorBy(e.coloredField, group); + }, + + handleSelectorHover(e, group, id){ + console.log('selector hovered', group, id, e); + this.cellCompositionVars[group].highlightLabel = e.hoveredLabel; + }, + /* cell composition */ diff --git a/src/components/researchPortal/ResearchSingleCellSelector.vue b/src/components/researchPortal/ResearchSingleCellSelector.vue new file mode 100644 index 000000000..3a6872958 --- /dev/null +++ b/src/components/researchPortal/ResearchSingleCellSelector.vue @@ -0,0 +1,300 @@ + + + + + + \ No newline at end of file From 5f7475b58f226a0684dbc1470092860a2e3e9613 Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Mon, 9 Dec 2024 09:57:12 -0500 Subject: [PATCH 09/10] cfde citation formatting --- src/utils/formatters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/formatters.js b/src/utils/formatters.js index 1849b465a..e3f3183ef 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -655,8 +655,8 @@ function BYORColumnFormatter(VALUE, KEY, CONFIG, PMAP, DATA_SCORES) {
${item.authors} ${item.publication}
${item.description}
From 7953d13024a958affaf28d396f6c1656aef7a7b4 Mon Sep 17 00:00:00 2001 From: Alex Shilin Date: Mon, 9 Dec 2024 13:01:19 -0500 Subject: [PATCH 10/10] adding support for query string params --- .../ResearchSingleCellBrowser.vue | 96 +++++++++---------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/src/components/researchPortal/ResearchSingleCellBrowser.vue b/src/components/researchPortal/ResearchSingleCellBrowser.vue index 853be2f93..9ef311c00 100644 --- a/src/components/researchPortal/ResearchSingleCellBrowser.vue +++ b/src/components/researchPortal/ResearchSingleCellBrowser.vue @@ -617,7 +617,6 @@ segmentByCounts: null, datasetId: null, - datasetName: null, cellTypeField: null, geneNames: [], @@ -626,7 +625,6 @@ dataLoaded: false, preloadItem: '', - tissueDisplay: '', highlightHoverTimeout: null, selectedTabs: {"a":"1", "b":"2"}, @@ -668,10 +666,14 @@ if(data.id===this.sectionId){ console.log(this.sectionId, 'Received on-select event:', data); this.datasetId = data.value; + if(this.renderConfig["parameters"]?.datasetId){ + keyParams.set({[this.renderConfig["parameters"]?.datasetId] : this.datasetId}); + } this.init(); } }, clean(){ + this.expressionStats = []; this.cellCompositionVars = { "a": { umapColors: null, @@ -704,19 +706,8 @@ } }, async init(){ - /* - if(this.data.length !== 1){ - if(this.datasetData) { - console.log('--->', this.datasetData); - }else{ - console.log("please select a dataset"); - return; - } - }else{ - this.datasetId = this.data[0].datasetId; - } - */ - + //check which components to enable based on cofig options + //all are enabled by default this.componentsConfig = this.renderConfig["components"]; this.showCellInfo = this.componentsConfig?.["cell info"]?.enabled ?? true; this.showCellProportion = this.componentsConfig?.["cell proportion"]?.enabled ?? true; @@ -725,9 +716,16 @@ this.presetsConfig = this.renderConfig["presets"]; - //if user has not selected a dataset from the list + //check for datasetId + /* it can come from multiple places + 1. 'on-select' event from byor + 2. query string param + 3. config preset + */ if(!this.datasetId || this.datasetId === ''){ - if(this.presetsConfig?.datasetId){ + if(keyParams[this.renderConfig["parameters"]?.datasetId]){ + this.datasetId = keyParams[this.renderConfig["parameters"].datasetId]; + }else if(this.presetsConfig?.datasetId){ this.datasetId = this.presetsConfig.datasetId }else{ console.log('select a dataset'); @@ -735,17 +733,21 @@ } } - this.clean(); - - console.log(`loading dataset: ${this.datasetData.datasetId}`); - console.log(' data', this.datasetData); + console.log(`requested dataset: ${this.datasetId}`); - this.tissueDisplay = this.datasetData["Tissue"]; - this.datasetName = this.datasetData["Name"]; - this.cellTypeField = this.presetsConfig?.["cell type label"]; - console.log("cellTypeField", this.cellTypeField, this.presetsConfig); - this.cellCompositionVars['a'].colorByLabel = this.cellTypeField; + //make sure it exists in the metadata + if(!this.data.find(x => x.datasetId === this.datasetId)){ + console.log('dataset', this.datasetId, 'not in collection'); + this.datasetId = null; + return; + } + console.log(' data', this.datasetData); + + //clear existing data + this.clean(); + + //fetch base data this.dataLoaded = false; this.preloadItem = 'fields'; this.rawData = await this.fetchFields(); @@ -762,10 +764,18 @@ //pre-calculate colors for fields in each category this.labelColors = this.calcLabelColors(this.rawData); + this.colorScalePlasmaColorsArray = d3.range(0, 1.01, 0.1).map(t => this.colorScalePlasma(t)).join(', '); this.getColorByOptions(); + //which label designates cell types + this.cellTypeField = this.presetsConfig?.["cell type label"]; + + console.log("cellTypeField", this.cellTypeField, this.presetsConfig); + + this.cellCompositionVars['a'].colorByLabel = this.cellTypeField; + this.selectColorBy(this.cellTypeField, 'a'); this.selectColorBy(this.cellTypeField, 'b'); @@ -786,17 +796,17 @@ console.log('cell proportion component disabled') } - this.expressionStats = []; - - - if(keyParams.gene){ - this.fetchGeneExpression(keyParams.gene.toUpperCase()); + //load gene data from parameters + if(this.renderConfig["parameters"]?.gene){ + if(keyParams[this.renderConfig["parameters"].gene]){ + this.fetchGeneExpression(keyParams.gene.toUpperCase()); + } } - + //load gene data from config if(this.presetsConfig?.["genes"]){ this.presetsConfig["genes"].forEach(async (gene) => { - await this.fetchGeneExpression(gene.toUpperCase()); + this.fetchGeneExpression(gene.toUpperCase()); }) } @@ -813,22 +823,10 @@ this.colorByOptions = colorByOptions2; }, - updateDataUrl(url, paramaters){ - let updatedUrl = url; - //tmp - //updatedUrl = updatedUrl.replace(`$datasetId`, this.datasetData['Tissue'].toLowerCase()); - - //use below once metadata is fixed - paramaters.forEach(param => { - if(param !== 'gene') - updatedUrl = updatedUrl.replace(`$${param}`, this.datasetData[param]); //.toLowerCase().replaceAll(" ", "_") - }) - return updatedUrl; - }, async fetchFields() { console.log('getting fields'); const fieldsDataPoint = this.renderConfig["data points"].find(x => x.role === "fields"); - const fieldsUrl = this.updateDataUrl(fieldsDataPoint.url, fieldsDataPoint.parameters); + const fieldsUrl = fieldsDataPoint.url.replace('$datasetId', this.datasetId); try { const response = await fetch(fieldsUrl); const rawData = await response.json(); @@ -842,7 +840,7 @@ async fetchCoordinates() { console.log('getting coordinates'); const coordinatesDataPoint = this.renderConfig["data points"].find(x => x.role === "coordinates"); - const coordinatesUrl = this.updateDataUrl(coordinatesDataPoint.url, coordinatesDataPoint.parameters); + const coordinatesUrl = coordinatesDataPoint.url.replace('$datasetId', this.datasetId); try { const response = await fetch(coordinatesUrl); const json = this.utils.dataConvert.tsv2Json(await response.text()); @@ -855,11 +853,11 @@ async fetchGeneExpression(gene){ console.log('fetchGeneExpression', gene); const expressionDataPoint = this.renderConfig["data points"].find(x => x.role === "expression"); - const expressionUrl = this.updateDataUrl(expressionDataPoint.url, expressionDataPoint.parameters); + const expressionUrl = expressionDataPoint.url.replace('$datasetId', this.datasetId).replace('$gene', gene); //this.isLoading = true; await Vue.nextTick(); try{ - const response = await fetch(expressionUrl+','+gene); + const response = await fetch(expressionUrl); const json = await response.json(); const expression = json.data[0]['expression']; this.geneNames.push(gene);