From e7568b3677fe0bd1f9f6ba757acdb962f2a0dc02 Mon Sep 17 00:00:00 2001
From: Remi  PLANEL <rplanel@pasteur.fr>
Date: Tue, 14 May 2024 09:58:01 +0200
Subject: [PATCH] WIP: display operon structure for each matrix

---
 components/OperonStructure.vue               |   9 +-
 components/content/PdockqMatrix.vue          | 192 ++++++++++++++-----
 components/content/SystemOperonStructure.vue |  35 +---
 types/structure.ts                           |   5 +
 4 files changed, 160 insertions(+), 81 deletions(-)

diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue
index 45458e0e..45869e83 100644
--- a/components/OperonStructure.vue
+++ b/components/OperonStructure.vue
@@ -8,19 +8,17 @@ import { useTextRotate, type TextRotateOuput } from '~/composables/useTextRotate
 
 
 interface Props {
-    genes: StructureOperonGeneWithImg[] | null
+    operonGenes: StructureOperonGeneWithImg[] | null
     maxOperonSize: number | undefined
     subsystems: string[]
 }
 const structureBasket = useStructuresBasket()
 
 const props = withDefaults(defineProps<Props>(), {
-    genes: null
+    operonGenes: null
 });
 
-const { genes: genesProps } = toRefs(props)
-// const refGenes = ref()
-// const height = ref<number>(200)
+const { operonGenes: genesProps } = toRefs(props)
 const svgRef = ref<SVGElement | null>(null)
 const structureHeight = ref(130)
 const geneHeight = ref(40)
@@ -34,7 +32,6 @@ const margin = ref<PlotMargin>({
 })
 const gbContainer = ref(null)
 const geneToHighlight = ref<string | null>(null)
-const snackbar = ref(false)
 const innerPaddingRatio = ref<number>(0.08)
 // const color = d3.scaleOrdinal(d3.schemeCategory10);
 const domain = computed(() => {
diff --git a/components/content/PdockqMatrix.vue b/components/content/PdockqMatrix.vue
index cfb7d98a..f08cd80d 100644
--- a/components/content/PdockqMatrix.vue
+++ b/components/content/PdockqMatrix.vue
@@ -5,15 +5,33 @@ import PlotFigure from "@/components/PlotFigure";
 // import { useMeiliSearchRef } from 'nuxt-meilisearch'
 import type { SearchResponse } from 'meilisearch'
 import { useRefinedUrl } from "@/composables/useRefinedUrl"
-import { useDisplay } from "vuetify";
 import { useStructuresBasket } from "~/stores/structuresBasket";
+import type { StructureItem, StructureItemWithImage, StructureOperonGeneWithImg } from "~/types/structure";
+import { joinURL } from 'ufo'
 
 interface Props {
     subsystem: string
+    system: string
+    genes: StructureOperonGeneWithImg[] | null
+    maxOperonSize: number | undefined
+    subsystems: string[]
+    monomerStructures: StructureItemWithImage[]
+
 }
-const { subsystem } = withDefaults(defineProps<Props>(), {
+const props = withDefaults(defineProps<Props>(), {
 });
-const { mobile } = useDisplay();
+const sizeGene = ref<number>(108)
+const system = toRef(props, "system")
+const subsystem = toRef(props, "subsystem")
+
+const maxOperonSize = toRef(props, "maxOperonSize")
+const operonGenes = toRef(props, "genes")
+const monomerStructures = toRef(props, "monomerStructures")
+const contentWidth = computed(() => {
+    const genesVal = toValue(operonGenes)
+    if (genesVal !== null) return genesVal.length * sizeGene.value
+    return 0
+})
 const { page } = useContent();
 const dbName = "structure"
 const filterBase = ref<string[]>([
@@ -24,6 +42,7 @@ const structureTitle = ref("Structure")
 // const structureUrls = ref<string[] | undefined>(undefined)
 const matrixElemSize = ref(50)
 const structureBasket = useStructuresBasket()
+const { wholeRow } = useColumnLayout({ contentWidth, widthThreshold: 600 })
 
 export interface PlotMargin {
     marginTop: number,
@@ -39,17 +58,16 @@ const margin = ref<PlotMargin>({
     marginLeft: 150
 })
 
-const data = ref<SearchResponse | undefined>(undefined)
+const data = ref<SearchResponse<StructureItem> | undefined>(undefined)
 const client = useMeiliSearchRef()
 const matrixPlot = ref()
-const snackbar = ref(false)
 
 onMounted(async () => {
     try {
-        const d = await client.index(toValue(dbName)).search("", {
+        const d = await client.index(toValue(dbName)).search<StructureItem>("", {
             facets: ["*"],
             filter: [
-                `system='${toValue(computedSystem)}' AND subsystem='${subsystem}'`,
+                `system='${toValue(computedSystem)}' AND subsystem='${toValue(subsystem)}'`,
                 ...toValue(filterBase)
             ],
         })
@@ -60,6 +78,9 @@ onMounted(async () => {
 })
 
 
+
+
+
 const computedSystem = computed(() => {
     const toValPage = toValue(page)
     return toValPage?.system ?? toValPage?.title ?? undefined
@@ -72,13 +93,12 @@ function extractGeneName(name: string) {
     if (name.includes("_")) {
         return name.split("_")[1]
     }
-    return undefined
+    return ''
 }
 
 const groupedPdocks = computed(() => {
     const toValData = toValue(data)
     if (toValData?.hits) {
-
         const flattenMap = toValData.hits.flatMap(({ system, pDockQ, pdb, nb_sys, proteins_in_the_prediction, system_genes }) => {
             if (proteins_in_the_prediction.length === 2) {
                 const sanitizedSystemGenes = system_genes.map(extractGeneName)
@@ -107,6 +127,84 @@ const groupedPdocks = computed(() => {
     } else { return [] }
 })
 
+
+const groupedPdocksProteinNames = computed<[string, Set<string>][]>(() => {
+    const groupedPdocksVal = toValue(groupedPdocks)
+    return groupedPdocksVal.map(([key, value]) => {
+        // get all the proteins name in values
+        const listProt = new Set(value.flatMap(d => d.proteins_in_the_prediction))
+        return [key, listProt]
+    })
+})
+/**
+ * Associate a monomer to each proteins found in dimers
+ */
+const proteinNameToMonomer = computed<[string, StructureItemWithImage[]][]>(() => {
+    const monomerStructuresVal = toValue(monomerStructures)
+    const groupedPdocksProteinNamesVal = toValue(groupedPdocksProteinNames)
+    return groupedPdocksProteinNamesVal.map(([key, protSet]) => {
+        let matchingMonomers: StructureItemWithImage[] = []
+        protSet.forEach(protName => {
+            const foundMonomer = monomerStructuresVal.find((monomer) => {
+                return monomer.proteins_in_the_prediction[0] === protName
+            })
+            if (foundMonomer !== undefined) matchingMonomers.push({ ...foundMonomer })
+        })
+        // for each elem in operon try to match a prot in the set
+        return [key, matchingMonomers]
+    })
+})
+
+/**
+ * Associate to each gene in operon to a monomer
+ */
+
+const operonStructurePerMatrix = computed<[string, StructureOperonGeneWithImg[]][]>(() => {
+    const proteinNameToMonomerVal = toValue(proteinNameToMonomer)
+    const operonGenesVal = toValue(operonGenes)
+    return proteinNameToMonomerVal.map(([key, monomers]) => {
+        if (operonGenesVal !== null) {
+            return [key, operonGenesVal.map(gene => {
+                const sanitizedGene = gene.gene
+                const sanitizedExangeableGenes = gene.exchangeables
+                const genesAndSynonymous = new Set([sanitizedGene, ...sanitizedExangeableGenes])
+                const matchingMonomer = monomers.find((monomer) => {
+                    const hasInSet = genesAndSynonymous.has(monomer.proteins_in_the_prediction[0])
+                    if (hasInSet) {
+                        return hasInSet
+                    } else {
+                        // try some less restrictive search
+                        return Array.from(genesAndSynonymous).find(dfModelGeneName => {
+                            if (dfModelGeneName !== undefined && monomer?.proteins_in_the_prediction !== undefined) {
+                                const protName = monomer.proteins_in_the_prediction[0]
+                                if (protName !== undefined) return dfModelGeneName.startsWith(protName)
+                            }
+                            return false
+                        }) !== undefined
+                    }
+
+                })
+                const rawImgUrl = joinURL(`/${toValue(system).toLowerCase()}`, matchingMonomer?.structImg ?? '')
+                const rawStructUrl = joinURL(`/${toValue(system).toLowerCase()}`, matchingMonomer?.structPath ?? '')
+                const { refinedUrl: structImgHref } = useRefinedUrl(rawImgUrl)
+                return { ...gene, gene: matchingMonomer ? matchingMonomer.proteins_in_the_prediction[0] : sanitizedGene, structImg: toValue(structImgHref), structPath: rawStructUrl }
+            })]
+        }
+        else { return [] }
+    })
+})
+
+
+const operonStrucutrePerMatrixObject = computed(() => {
+    const operonStructurePerMatrixVal = toValue(operonStructurePerMatrix)
+    const object = Object.fromEntries(operonStructurePerMatrixVal)
+    return object
+})
+
+function getOperonStructure(key: string) {
+    const operonStrucutrePerMatrixObjectVal = toValue(operonStrucutrePerMatrixObject)
+    return operonStrucutrePerMatrixObjectVal[key]
+}
 const computedPDocksMatrixPlotOptions = computed(() => {
     const domain = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
     const scaleLinear = d3.scaleLinear([0.3, 0.9], [1, 0])
@@ -115,28 +213,23 @@ const computedPDocksMatrixPlotOptions = computed(() => {
     })
     const matrixElemSizeVal = toValue(matrixElemSize)
     const { marginBottom, marginLeft, marginRight, marginTop } = toValue(margin)
-    return toValue(groupedPdocks).map((matrix) => {
-        const plotWidth = matrix[1][0].system_genes.length * matrixElemSizeVal + marginLeft + marginRight
-        console.log(matrix[0])
-        console.log(matrix[1][0].system_genes.length)
-        console.log(plotWidth)
+    return toValue(groupedPdocks).map(([key, group]) => {
+        const plotWidth = group[0].system_genes.length * matrixElemSizeVal + marginLeft + marginRight
         return {
             width: plotWidth,
-            height: matrix[1][0].system_genes.length * matrixElemSizeVal + marginTop + marginBottom,
-            title: matrix[0],
+            height: group[0].system_genes.length * matrixElemSizeVal + marginTop + marginBottom,
+            title: key,
             padding: 0,
             marginTop,
             marginLeft,
             marginRight,
             marginBottom,
             grid: true,
-
             x: { axis: "top", tickRotate: 45 },
-            legend: { label: matrix[0] },
+            legend: { label: key },
             color: { type: "threshold", domain: [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], range: ["lightgrey", ...range], legend: true },
             marks: [
-                // Plot.frame(),
-                on(Plot.cell(toValue(matrix[1]), {
+                on(Plot.cell(toValue(group), {
                     x: (d) => d.protX,
                     y: (d) => d.protY,
                     fill: "pDockQ",
@@ -192,7 +285,6 @@ function pdbNameToCif(pdbPath: string) {
 function buildStructureUrl(item) {
     return [`/${item.system.toLowerCase()}/${pdbNameToCif(item.pdb)}`, `/${item.system}/${item.pdb}`]
 }
-
 function displayStructure(item) {
     structureBasket.set(
         buildStructureUrl(item).map(url => {
@@ -205,33 +297,37 @@ const show = ref(false)
 
 </script>
 <template>
+    <v-row v-if="computedSystem !== null && groupedPdocks?.length > 0"
+        v-for="option, i in computedPDocksMatrixPlotOptions" :key="option.legend.label" align="top">
 
-    <v-card v-if="computedSystem !== null && groupedPdocks?.length > 0"
-        v-for="option in computedPDocksMatrixPlotOptions" :key="option.legend.label" flat color="transparent">
-
-        <v-card-item>
-            <v-card-title>Dimer pDockQ matrix <v-btn size="x-small" density="comfortable" variant="tonal"
-                    :icon="show ? 'mdi-chevron-up' : 'mdi-help'" @click="show = !show"></v-btn>
-            </v-card-title>
-        </v-card-item>
-        <v-expand-transition>
-            <div v-show="show">
-                <v-card-text class="pt-1">
-                    <v-alert type="info" variant="tonal">
-                        <p>HeatMap that represent bla bla.</p>
-                        <p>You can click on each cell to display dimer predicted structure</p>
-                    </v-alert>
+        <v-col :cols="wholeRow ? 12 : 6" align-self="stretch" style="background-color: transparent;">
+            <OperonStructure :operonGenes="getOperonStructure(option.legend.label)" :subsystem
+                :max-operon-size="maxOperonSize" :subsystems />
+
+        </v-col>
+        <v-col :cols="wholeRow ? 12 : 6">
+            <v-card flat color="transparent">
+                <v-card-item>
+                    <v-card-title>Dimer pDockQ matrix <v-btn size="x-small" density="comfortable" variant="tonal"
+                            :icon="show ? 'mdi-chevron-up' : 'mdi-help'" @click="show = !show"></v-btn>
+                    </v-card-title>
+                </v-card-item>
+                <v-expand-transition>
+                    <div v-show="show">
+                        <v-card-text class="pt-1">
+                            <v-alert type="info" variant="tonal">
+                                <p>You can click on each cell to display dimer predicted structure</p>
+                            </v-alert>
+                        </v-card-text>
+                    </div>
+                </v-expand-transition>
+                <v-card-text>
+                    <PlotFigure ref="matrixPlot" :options="unref(option)" defer class="pdockq-plot"
+                        :data-label="option.legend.label"></PlotFigure>
                 </v-card-text>
-            </div>
-        </v-expand-transition>
-        <v-card-text>
-            <PlotFigure ref="matrixPlot" :options="unref(option)" defer class="pdockq-plot"
-                :data-label="option.legend.label"></PlotFigure>
-        </v-card-text>
-    </v-card>
-    <v-card v-else flat color="transparent">
-        <v-card-text>
-            <v-alert type="info" variant="tonal">No matrix available for this system.</v-alert>
-        </v-card-text>
-    </v-card>
+
+            </v-card>
+
+        </v-col>
+    </v-row>
 </template>
\ No newline at end of file
diff --git a/components/content/SystemOperonStructure.vue b/components/content/SystemOperonStructure.vue
index 53fa8ee5..b9842f0f 100644
--- a/components/content/SystemOperonStructure.vue
+++ b/components/content/SystemOperonStructure.vue
@@ -1,9 +1,7 @@
 <script setup lang="ts">
-import * as d3 from "d3";
-import type { StructureItem, StructureOperonGene, StructureOperonGeneWithImg } from "../../types/structure";
+import type { StructureItem, StructureItemWithImage, StructureOperonGene, StructureOperonGeneWithImg } from "../../types/structure";
 import type { SearchResponse } from "meilisearch";
 import { joinURL } from 'ufo'
-import { useColumnLayout } from '~/composables/useColumnLayout';
 
 
 interface Props {
@@ -28,7 +26,7 @@ const maxOperonSize = toRef(props, "maxOperonSize")
 const msIndexName = ref<'systemoperonstruct'>("systemoperonstruct")
 const msResponse = ref<SearchResponse<StructureOperonGene> | undefined>(undefined)
 const allSubsystems = ref<SearchResponse<StructureOperonGene> | undefined>(undefined)
-const monomerStructures = computed(() => {
+const monomerStructures = computed<StructureItemWithImage[]>(() => {
     const structuresVal = toValue(structures)
     if (structuresVal) {
         return structuresVal.filter(struct => {
@@ -54,13 +52,12 @@ function extractGeneName(name: string) {
     if (name.includes("_")) {
         return name.split("_")[1]
     }
-    return undefined
+    return ''
 }
 
 const sanitizedHits = computed<StructureOperonGeneWithImg[]>(() => {
     const toValMsResponse = toValue(msResponse)
     if (toValMsResponse && toValMsResponse?.hits?.length > 0) {
-        console.log(toValMsResponse.hits)
         return toValMsResponse.hits.map(hit => {
             // get structure information for this prot
             const monomerStructuresVal = toValue(monomerStructures)
@@ -85,10 +82,10 @@ const sanitizedHits = computed<StructureOperonGeneWithImg[]>(() => {
                 const rawImgUrl = joinURL(`/${system.toLowerCase()}`, struct?.structImg ?? '')
                 const rawStructUrl = joinURL(`/${system.toLowerCase()}`, struct?.structPath ?? '')
                 const { refinedUrl: structImgHref } = useRefinedUrl(rawImgUrl)
-                return { ...hit, gene: sanitizedGene, structImg: toValue(structImgHref), structPath: rawStructUrl }
+                return { ...hit, gene: sanitizedGene, exchangeables: sanitizedExangeableGenes, structImg: toValue(structImgHref), structPath: rawStructUrl }
             }
             else {
-                return { ...hit, gene: sanitizedGene }
+                return { ...hit, gene: sanitizedGene, exchangeables: sanitizedExangeableGenes }
             }
         })
     } else {
@@ -106,15 +103,6 @@ const sanitizedSubsystem = computed(() => {
     return subsystem
 })
 
-const contentWidth = computed(() => {
-    console.log(subsystem)
-    console.log(sanitizedHits.value)
-    return sanitizedHits.value.length * sizeGene.value
-})
-
-const { wholeRow } = useColumnLayout({ contentWidth, widthThreshold: 600 })
-
-
 onMounted(() => {
     fetchOperonStructure()
     fetchSubsytems()
@@ -178,16 +166,9 @@ async function fetchSubsytems() {
             <v-card-title>{{ subsystem }}</v-card-title>
         </v-card-item>
         <v-card-text>
-            <v-row align="top">
-                <v-col :cols="wholeRow ? 12 : 6" align-self="stretch" style="background-color: transparent;">
-                    <OperonStructure :genes="sanitizedHits" :system :subsystem :max-operon-size="maxOperonSize"
-                        :subsystems />
-                </v-col>
-
-                <v-col :cols="wholeRow ? 12 : 6">
-                    <PdockqMatrix :subsystem="subsystem" />
-                </v-col>
-            </v-row>
+            <PdockqMatrix :genes="sanitizedHits" :max-operon-size :subsystem="subsystem" :subsystems :monomer-structures
+                :system />
+
         </v-card-text>
     </v-card>
 </template>
\ No newline at end of file
diff --git a/types/structure.ts b/types/structure.ts
index 5753b726..0f4141d9 100644
--- a/types/structure.ts
+++ b/types/structure.ts
@@ -65,4 +65,9 @@ export interface StructureItem {
     OrbA: string
     Anti_BREX: string
     // "structuresUrls": ["/wiki/Avs/AVAST_I.AVAST_I__Avs1A.0.V.cif", "/wiki/Avs/AVAST_I.AVAST_I__Avs1A.0.V.pdb"]
+}
+
+export interface StructureItemWithImage extends StructureItem {
+    structImg: string
+    structPath: string
 }
\ No newline at end of file
-- 
GitLab