From a9d28f575dcf2ee3bab5cb2638fe71cd9980bd2d Mon Sep 17 00:00:00 2001
From: Remi  PLANEL <rplanel@pasteur.fr>
Date: Mon, 29 Apr 2024 16:04:03 +0200
Subject: [PATCH] use d3 link and transition

---
 components/OperonStructure.vue | 242 ++++++++++++++++++++++++---------
 types/structure.ts             |   1 +
 2 files changed, 182 insertions(+), 61 deletions(-)

diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue
index 7bf2dfb5..1cf816cf 100644
--- a/components/OperonStructure.vue
+++ b/components/OperonStructure.vue
@@ -14,6 +14,9 @@ const structureBasket = useStructuresBasket()
 const props = withDefaults(defineProps<Props>(), {
     genes: null
 });
+
+const { genes: genesProps } = toRefs(props)
+// const refGenes = ref()
 const height = ref<number>(200)
 const svgRef = ref<SVGElement | null>(null)
 const margin = ref<PlotMargin>({
@@ -23,6 +26,8 @@ const margin = ref<PlotMargin>({
     marginLeft: 7,
 })
 
+const geneToHighlight = ref<string | null>(null)
+
 const snackbar = ref(false)
 const color = d3.scaleOrdinal(d3.schemeCategory10);
 const plotHeight = computed(() => {
@@ -63,6 +68,7 @@ const xScaleGenes = computed(() => {
 })
 
 const yScale = ref(d3.scaleBand()
+    .paddingInner(0.5)
     .domain(['img', 'buff', 'buff2', 'gene'])
     .range([toValue(margin).marginTop, toValue(height)]));
 const gbContainer = ref(null)
@@ -77,8 +83,8 @@ const computedPlotWidth = computed(() => {
 
 
 const computedGenes = computed<StructureOperonGene[]>(() => {
-    const genes = toValue(props.genes)
-    if (genes !== null) {
+    const genes = toValue(genesProps)
+    if (genes !== null && genes?.length > 0) {
         let currentSumSize = 0
         return genes.map(d => {
             const size = d?.size ?? 10
@@ -87,14 +93,16 @@ const computedGenes = computed<StructureOperonGene[]>(() => {
             return {
                 ...d,
                 size,
-                position
+                position,
+                highlight: geneToHighlight.value === d.gene
             }
         })
     }
     else { return [] }
-})
 
 
+
+})
 const structureVersion = computed(() => {
     const genesVal = toValue(computedGenes)
     if (genesVal?.length > 0) {
@@ -105,8 +113,33 @@ const structureVersion = computed(() => {
     }
 })
 
+const structureNodes = computed<StructureOperonGeneWithCoordinate[]>(() => {
+    const genes = toValue(computedGenes)
+    const xScaleVal = toValue(xScale)
+    const yScaleVal = toValue(yScale)
+    if (genes !== null) {
+        return genes.map(d => {
+            const r = d3.min([toValue(xScale).bandwidth(), toValue(yScale).step() * 3])
+            // const geneWidth = xScaleVal(d.size)
+            // const position = xScaleVal(d.position)
+            // const geneCenter = xScaleVal(d.position) + geneWidth / 2
+            // const xPostion = geneCenter - r / 2
+            return {
+                ...d,
+                r: r,
+                // x: xPostion > 0 ? xPostion : 0,
+                x: xScaleVal(d.gene),
+                y: yScaleVal('img'),
+                width: toValue(xScale).bandwidth(),
+                // width: r,
+                height: yScaleVal.step() * 3
+            }
+        })
+    }
+    else { return [] }
+})
 
-const genesWithCoord = computed<StructureOperonGeneWithCoordinate[]>(() => {
+const geneNodes = computed<StructureOperonGeneWithCoordinate[]>(() => {
     const genes = toValue(computedGenes)
     const xScaleVal = toValue(xScaleGenes)
     const yScaleVal = toValue(yScale)
@@ -123,11 +156,34 @@ const genesWithCoord = computed<StructureOperonGeneWithCoordinate[]>(() => {
     }
     else { return [] }
 })
+
+const linksGenesStruct = computed(() => {
+    const geneNodesVal = toValue(geneNodes)
+    const structureNodesVal = toValue(structureNodes)
+
+    return d3.zip(geneNodesVal, structureNodesVal).map(([source, target]) => {
+        return {
+            source: [source.x + source.width / 2, source.y],
+            target: [target.x + target.width / 2, target.y + target.height / 2 + 30],
+            highlight: source?.highlight || target?.highlight
+        }
+    })
+
+})
+
+
 onMounted(() => {
+    // const genePropsVal = toValue(genesProps)
+    // refGenes.value = genePropsVal?.map(d => d)
     draw()
 })
 
-watch(genesWithCoord, () => {
+watch(structureNodes, () => {
+    draw()
+})
+
+
+watch(geneNodes, () => {
     draw()
 })
 
@@ -156,49 +212,130 @@ function draw() {
             .attr("transform", 'rotate(-20)')
             .attr("text-anchor", "start")
 
-        let gGenes = createOrSelect(svg, "g", "genes")
-        gGenes
+        let gOperon = createOrSelect(svg, "g", "operon")
+        gOperon
             .attr("transform", `translate(${marginLeft},0)`)
-            .call(drawGenes, xScale, yScale)
+            .call(drawLinks)
+            .call(drawGenes)
+            .call(drawStructure)
+
     }
 }
 
-function drawGenes(genesGroup: d3.Selection<SVGElement, any, SVGElement, any>) {
-    const data = toValue(genesWithCoord)
-    const genesSelection = genesGroup
-        .selectAll("g.operon-item") // get all "existing" lines in svg
-        .data<StructureOperonGeneWithCoordinate>(data) // sync them with our data
-        .join(
-            enter => {
-                const gOperonItem = enter.append("g")
-                    .classed("operon-item", true);
+function adjacentlinks(nodes: Record<string, any>) {
+    const links = []
+    for (let i = 0; i < nodes.length; i++) {
+        if (i < nodes.length - 1) {
+            links.push({ index: i, source: nodes[i], target: nodes[+1] })
+        }
+    }
+    return links
+}
 
-                // gene grp
-                const gGene = gOperonItem.append("g")
-                    .classed("gene-grp", true)
-                gGene
-                    .append("path")
-                    .classed("gene", true)
+function drawLinks(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) {
+    const stroke = "#555" // stroke for links
+    const strokeWidth = 1.5 // stroke width for links
+    const strokeOpacity = 0.4 // stroke opacity for links
+    operonGroup
+        .selectAll("path.link")
+        .data(linksGenesStruct.value)
+        .join("path")
+        .classed("link", true)
+        .attr("fill", "none")
+        .attr("stroke", "currentColor")
+        .attr("stroke-opacity", d => d.highlight ? 0.6 : strokeOpacity)
+        .attr("stroke-width", d => d.highlight ? strokeWidth + 2 : strokeWidth)
+        .attr("d", d3.link(d3.curveBumpY))
+}
+
+function drawStructure(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) {
+    const structureNodeVal = toValue(structureNodes)
+
+    // const totalSize = domainGenes.value[1]
+    // const structureLinks = adjacentlinks(structures)
+    // const sim = d3.forceSimulation(structureNodeVal)
+    // .force("link", d3.forceLink(structureLinks))
+    //     .force("collide", d3.forceCollide((d) => d.r))
+    //     .stop()
+    //     .tick(10);
+    // sim.tick()
 
+
+    const structureSelection = operonGroup
+        .selectAll("g.structure") // get all "existing" lines in svg
+        .data<StructureOperonGeneWithCoordinate>(structureNodeVal) // sync them with our data
+        .join(
+            enter => {
+                const gStructure = enter.append("g")
+                    .classed("structure", true);
                 // img group
-                gOperonItem
-                    .append("g").classed("img", true)
+                gStructure
                     .append("image")
                     .on("mouseover", function (event) {
+                        const srcSelection = d3.select(event.srcElement)
                         const target = d3.select(event.srcElement.parentElement)
+                        const node = srcSelection.data()
+                        geneToHighlight.value = node[0].gene
+
                         target
-                            // .attr("stroke-width", 4)
-                            // .attr("stroke", "darkred")
                             .attr("cursor", "pointer")
                     })
+
                     .on("mouseout", function (event) {
                         const target = d3.select(event.srcElement.parentElement)
                         target
-                            // .attr("stroke-width", 0)
-                            // .attr("stroke", null)
                             .attr("cursor", "unset")
+                        geneToHighlight.value = null
+
                     })
 
+                return gStructure
+            },
+            update => update,
+            exit => exit.remove()
+        )
+    const imageSelection = structureSelection
+        .attr("transform", d => `translate(0,${toValue(yScale)("img")})`)
+        .select("image")
+    imageSelection
+        .transition()
+        .attr("transform", d => `translate(${d.x},${d.highlight ? -10 : 0})`)
+    imageSelection
+        .attr("href", d => d?.structImg ?? null)
+        .attr("width", d => d.width)
+        .attr("height", d => d.height)
+        .attr("preserveAspectRatio", "xMidYMid meet")
+        .on("click", function (event) {
+            const data = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(this).data()
+            structureBasket.set(data.map(s => s?.structPath ?? ''))
+
+        })
+
+
+}
+
+
+function drawGenes(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) {
+    const genesWithCoordVal = toValue(geneNodes)
+    const genes = genesWithCoordVal
+
+    const links = adjacentlinks(genes)
+
+
+    const genesSelection = operonGroup
+        .selectAll("g.operon-item") // get all "existing" lines in svg
+        .data<StructureOperonGeneWithCoordinate>(genes) // sync them with our data
+        .join(
+            enter => {
+                const gOperonItem = enter.append("g")
+                    .classed("operon-item", true);
+
+                // gene grp
+                const gGene = gOperonItem.append("g")
+                    .classed("gene-grp", true)
+                gGene
+                    .append("path")
+                    .classed("gene", true)
 
                 gOperonItem.append("text")
                     // .attr("fill", "white")
@@ -215,45 +352,28 @@ function drawGenes(genesGroup: d3.Selection<SVGElement, any, SVGElement, any>) {
             exit => exit.remove()
         )
     genesSelection.select("g.gene-grp").attr("transform", d => `translate(${d.x}, 0)`)
-    genesSelection.select("g.img")
-        .attr("transform", d => `translate(${xScale.value(d.gene)})`)
-        .select("image")
-        .attr("transform", d => `translate(0, ${toValue(yScale)("img")})`)
-        .attr("href", d => d?.structImg ?? null)
-        .attr("width", toValue(xScale).bandwidth())
-        .attr("height", toValue(yScale).step() * 3)
-        .attr("preserveAspectRatio", "xMidYMid meet")
-        .on("click", function (event) {
-            const data = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(this).data()
-            structureBasket.set(data.map(s => s?.structPath ?? ''))
 
-        })
 
 
-    genesSelection.select("g.gene-grp").select("path.gene")
+    genesSelection.select("g.gene-grp")
+        .select("path.gene")
+        // .attr("stroke", d => d?.highlight ? d3.color(color(d.system))?.darker() : null)
+        .attr("stroke-width", d => d?.highlight ? 4 : 0)
         .attr("transform", d => `translate(0, ${d.y})`)
-        .attr("fill", d => color(d.system))
+        .attr("fill", d => d?.highlight ? d3.color(color(d.system))?.brighter() : color(d.system))
         .attr("d", d => drawGene(d).toString())
 
-    genesSelection.select("line")
-        //  x1="0" y1="80" x2="100" y2="20" stroke="black"
-        .attr("x1", d => xScale.value(d.gene) + xScale.value.bandwidth() / 2)
-        .attr("y1", d => yScale.value("buff2") + yScale.value.bandwidth() / 3)
-        .attr("x2", d => xScaleGenes.value(d.position) + xScaleGenes.value(d.size / 2))
-        .attr("y2", d => yScale.value("gene") - 2)
-        .attr("stroke", "currentColor")
 
-}
+    function drawGene({ width, height }) {
+        const context = d3.path()
+        context.moveTo(0, 0)
+        context.lineTo(width, 0)
+        context.lineTo(width, height)
+        context.lineTo(0, height)
+        context.closePath()
 
-function drawGene({ width, height }) {
-    const context = d3.path()
-    context.moveTo(0, 0)
-    context.lineTo(width, 0)
-    context.lineTo(width, height)
-    context.lineTo(0, height)
-    context.closePath()
-
-    return context
+        return context
+    }
 }
 
 </script>
diff --git a/types/structure.ts b/types/structure.ts
index e622d92c..2f5997a3 100644
--- a/types/structure.ts
+++ b/types/structure.ts
@@ -7,6 +7,7 @@ export interface StructureOperonGene {
     exchangeables: string[]
     size: number
     position: number
+    highlight?: boolean
 
 }
 
-- 
GitLab