From 7e565d7613c3a2b347e95714cab8f354dec0abee Mon Sep 17 00:00:00 2001
From: Remi  PLANEL <rplanel@pasteur.fr>
Date: Thu, 2 May 2024 16:46:50 +0200
Subject: [PATCH] auto multiline

---
 components/OperonStructure.vue |  50 +++++++---
 composables/useTextRotate.ts   | 168 +++++++++++++++++++++++++--------
 types/structure.ts             |   6 +-
 3 files changed, 172 insertions(+), 52 deletions(-)

diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue
index 87631009..e34b03ca 100644
--- a/components/OperonStructure.vue
+++ b/components/OperonStructure.vue
@@ -2,7 +2,7 @@
 import { useElementSize } from '@vueuse/core'
 import * as d3 from "d3";
 import type { PlotMargin } from "../types/plot";
-import type { StructureGeneLinks, StructureOperonGene, StructureOperonGeneWithCoordinate, StructureOperonGeneWithImg } from "../types/structure"
+import type { StructureGeneLinks, StructureOperonGene, StructureOperonGeneLabels, StructureOperonGeneWithCoordinate, StructureOperonGeneWithImg } from "../types/structure"
 import { useStructuresBasket } from '~/stores/structuresBasket';
 import { useTextRotate, type TextRotateOuput } from '~/composables/useTextRotate';
 
@@ -208,10 +208,10 @@ const linksGenesStruct = computed<StructureGeneLinks[]>(() => {
 
 })
 
-const genesLabel = computed(() => {
+const genesLabel = computed<StructureOperonGeneLabels[]>(() => {
     const geneNodesVal = toValue(geneNodesWithY)
     const yScaleVal = toValue(yScale)
-    return geneNodesVal.map(d => ({ ...d, y: yScaleVal.label }))
+    return geneNodesVal.map(d => ({ ...d, y: yScaleVal.label, sanitizedLabel: d.gene.replace(/_/g, ' ') }))
 })
 
 onMounted(() => {
@@ -221,7 +221,7 @@ const genesTextRotate = ref<Record<string, TextRotateOuput>>({})
 watch(genesLabel, (newGenesLabel) => {
     const genesTextRotateVal = toValue(genesTextRotate)
     newGenesLabel.forEach(el => {
-        const textRotate = useTextRotate({ availableWidth: el.width })
+        const textRotate = useTextRotate({ availableWidth: el.width, text: el.sanitizedLabel })
         genesTextRotateVal[el.id.toString()] = textRotate
     })
 })
@@ -419,19 +419,19 @@ function geneTitle(d: StructureOperonGeneWithCoordinate) {
 function drawGenesLabel(operonGroup: d3.Selection<SVGGElement, any, SVGElement | null, any>) {
     const genes = toValue(genesLabel)
     const updateSelection = operonGroup
-        .selectAll<SVGGElement, StructureOperonGeneWithCoordinate>("g.gene-label")
-        .data<StructureOperonGeneWithCoordinate>(genes)
+        .selectAll<SVGGElement, StructureOperonGeneLabels>("g.gene-label")
+        .data<StructureOperonGeneLabels>(genes)
         .join(
             enter => {
                 const labelSelection = enter
                     .append("g")
                     .classed("gene-label", true)
                     .on("click", function (event) {
-                        const data = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(this).data()
+                        const data = d3.select<SVGElement, StructureOperonGeneLabels>(this).data()
                         structureBasket.set(data.map(s => s?.structPath ?? ''))
                     })
                     .on("mouseover", function (event) {
-                        const srcSelection = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(event.srcElement)
+                        const srcSelection = d3.select<SVGElement, StructureOperonGeneLabels>(event.srcElement)
                         const node = srcSelection.data()
                         geneToHighlight.value = node[0].gene
                     })
@@ -454,19 +454,47 @@ function drawGenesLabel(operonGroup: d3.Selection<SVGGElement, any, SVGElement |
     updateSelection
         .attr("cursor", d => d.highlight ? "pointer" : null)
         .select<SVGTextElement>("text")
+        .attr("style", d => d.highlight ? "font-weight: 700" : null)
+        // .text(d => d.sanitizedLabel)
         .attr("transform", function (d) {
             const genesTextRotateVal = toValue(genesTextRotate)
             const geneId = d.id.toString()
             const textRotate = genesTextRotateVal?.[geneId]
             if (textRotate !== undefined) {
-                const { rotate, width, setTextElem, textWidth } = textRotate
+                const { rotate, width, setTextElem, wrappedText } = textRotate
                 setTextElem(this)
                 return `translate(${d.x + d.width / 2 - toValue(width) / 2},${d.y}) rotate(${toValue(rotate)}) `
             }
             return null
         })
-        .attr("style", d => d.highlight ? "font-weight: 700" : null)
-        .text(d => d.gene)
+        // .html(function (d) {
+        //     console.log("html-----------------------------------")
+
+        //     const genesTextRotateVal = toValue(genesTextRotate)
+        //     const geneId = d.id.toString()
+        //     const textRotate = genesTextRotateVal?.[geneId]
+        //     if (textRotate !== undefined) {
+        //         // if (false) {
+        //         const { wrappedText, setTextElem, rotate } = textRotate
+        //         setTextElem(this)
+        //         console.log("rotate = ", rotate)
+        //         const wrappedTextVal = toValue(wrappedText)
+        //         if (wrappedTextVal.length > 1) {
+        //             return wrappedTextVal.reduce((acc, curr, i) => {
+        //                 let str = `${acc}\n<tspan x="0" y="${i * 16}">${curr}</tspan>`
+        //                 // console.log("str: ", str)
+        //                 return str
+        //             }, '')
+        //         }
+        //         else {
+        //             return wrappedTextVal.join(" ")
+        //         }
+        //     } else {
+        //         return d.sanitizedLabel
+        //     }
+        // })
+
+
     updateSelection.select("title").text(d => operonTitle(d))
 
 }
diff --git a/composables/useTextRotate.ts b/composables/useTextRotate.ts
index 107f215b..f4743794 100644
--- a/composables/useTextRotate.ts
+++ b/composables/useTextRotate.ts
@@ -1,77 +1,167 @@
-import { useElementSize } from '@vueuse/core'
+import * as d3 from "d3";
 
 
 export interface TextRotateInput {
     availableWidth: MaybeRef<number>
+    text: MaybeRef<string>
 }
 
 export interface TextRotateOuput {
     rotate: ComputedRef<number>
-    width: Ref<number>
-    height: Ref<number>
-    textWidth: ComputedRef<number>,
+    width: ComputedRef<number>
+    height: ComputedRef<number>
+    wrappedText: ComputedRef<string[]>
     setTextElem: (newTextElement: MaybeRef<SVGTextElement>) => void
 }
 
-export function useTextRotate({ availableWidth }: TextRotateInput): TextRotateOuput {
-    const width = ref<number>(0)
-    const height = ref<number>(20)
+export function useTextRotate({ availableWidth, text }: TextRotateInput): TextRotateOuput {
+    // const width = ref<number>(0)
+    // const height = ref<number>(20)
     const textElement = ref<SVGTextElement | undefined>(undefined)
     const availableWidthVal = toValue(availableWidth)
+
+    const words = computed(() => {
+        const textVal = toValue(text)
+        if (textVal !== null) {
+            return textVal.split(" ")
+        }
+        return []
+    })
+
+    const wrappedText = computed(() => {
+        const textElementVal = toValue(textElement)
+
+        if (textElementVal !== undefined) {
+            const wordsVal = toValue(words)
+            const wordList = wordsVal.map(d => d).reverse()
+            const availableWidthVal = toValue(availableWidth)
+            let wrappedTextList = []
+            let line = []
+            let word: string | undefined = undefined
+            let maxWidth = 0
+            while (word = wordList.pop()) {
+                line.push(word)
+                textElementVal.textContent = line.join(" ")
+                const width = textElementVal.getComputedTextLength()
+                textElementVal.textContent = ''
+                if (width > maxWidth) {
+                    maxWidth = width
+                }
+                if (width > availableWidthVal) {
+                    if (line.length === 1) {
+                        return [toValue(text)]
+                    }
+                    else {
+                        const w = line.pop()
+                        if (w !== undefined) {
+                            wordList.push(w)
+                            wrappedTextList.push(line.join(" "))
+                            line = []
+                        }
+                    }
+                }
+                else {
+                    if (wordList.length === 0) {
+                        wrappedTextList.push(line.join(" "))
+                    }
+                }
+            }
+            textElementVal.textContent = ''
+            return wrappedTextList
+        }
+        return ['']
+    })
+
+
     const textWidth = computed(() => {
         const textElementVal = toValue(textElement)
         if (textElementVal !== undefined) {
-            const { width } = textElementVal.getBBox()
+            textElementVal.textContent = toValue(text)
+            const width = textElementVal.getComputedTextLength()
+            textElementVal.textContent = ""
             return width
         }
         return 0
     })
+
+    const width = computed(() => {
+        const textElementVal = toValue(textElement)
+        const wrappedTextVal = toValue(wrappedText)
+        const rotateRadVal = toValue(rotateRad)
+
+        if (rotateRadVal === Math.PI / 2) return 0
+
+        if (textElementVal !== undefined) {
+            if (wrappedTextVal.length === 1) {
+                textElementVal.textContent = wrappedTextVal[0]
+                const w = textElementVal.getComputedTextLength()
+                textElementVal.textContent = ''
+                return w
+            }
+            else {
+                // textElemSelection.selectAll()
+                let maxWidth = 0
+                for (let i = 0; i < wrappedTextVal.length; i++) {
+                    textElementVal.textContent = wrappedTextVal[i]
+                    const width = textElementVal.getComputedTextLength()
+                    if (width > maxWidth) {
+                        maxWidth = width
+                    }
+                    // textElemSelection.append("tspan").attr("x", 0).attr("y", 16 * i).text(wrappedTextVal[i])
+                }
+                textElementVal.textContent = ''
+                return maxWidth
+            }
+        }
+        else { return 0 }
+    })
+
+    const height = computed(() => {
+        const rotateRadVal = toValue(rotateRad)
+        const wrappedTextVal = toValue(wrappedText)
+        if (wrappedTextVal.length > 1) {
+            return 16 * wrappedTextVal.length + 10
+        }
+        else {
+            if (rotateRadVal === Math.PI / 2) {
+                return toValue(textWidth)
+            }
+            else {
+                return 16
+            }
+
+        }
+    })
+
     function setTextElem(newTextElement: MaybeRef<SVGTextElement>) {
         textElement.value = toValue(newTextElement)
     }
 
     const rotateRad = computed(() => {
-        const textWidthSide = textWidth.value / Math.sqrt(2)
+        const wrappedTextVal = toValue(wrappedText)
         const textWidthVal = toValue(textWidth)
-        if (textWidthVal === 0) return 0
-        if (availableWidthVal > textWidthVal) {
-            width.value = textWidth.value
-            height.value = 20
-            return 0
-        }
-        if (textWidthSide < availableWidthVal) {
-            width.value = textWidthSide
-            height.value = textWidthSide
-            return Math.PI / 4
-        }
-        else {
-            width.value = 0
-            height.value = textWidth.value
+        if (wrappedTextVal.length === 1 && availableWidthVal < textWidthVal) {
             return Math.PI / 2
         }
+        return 0
     })
 
     const rotate = computed(() => {
         return rotateRad.value * 180 / Math.PI
     })
 
-    watch(rotateRad, (newVal) => {
-        const textWidthVal = toValue(textWidth)
-        if (newVal === 0) {
-            width.value = textWidthVal
-            height.value = 20
-        }
-        else if (newVal > Math.PI / 4) {
-            width.value = 15
-            height.value = textWidthVal + 100
-        }
-        else {
-            const w = Math.sin(newVal) * textWidthVal
-            const h = Math.cos(newVal) * textWidthVal
-            height.value = h + 100
-            width.value = w
+ 
+
+    watchEffect(() => {
+        const textElementVal = toValue(textElement)
+        const wrappedTextVal = toValue(wrappedText)
+        if (textElementVal !== undefined) {
+            const textElemSelection = d3.select(textElementVal)
+            for (let i = 0; i < wrappedTextVal.length; i++) {
+                textElemSelection.append("tspan").attr("x", 0).attr("y", 16 * i).text(wrappedTextVal[i])
+            }
         }
     })
 
-    return { rotate, width, height, textWidth, setTextElem }
+    return { rotate, width, height, wrappedText, setTextElem }
 }
\ No newline at end of file
diff --git a/types/structure.ts b/types/structure.ts
index b61ea45c..67004bdd 100644
--- a/types/structure.ts
+++ b/types/structure.ts
@@ -22,8 +22,10 @@ export interface StructureOperonGeneWithCoordinate extends StructureOperonGeneWi
     x: number
     y: number
     labelHeight: number,
-    // labelWidth: number,
-    // rotate: number
+}
+
+export interface StructureOperonGeneLabels extends StructureOperonGeneWithCoordinate {
+    sanitizedLabel: string
 }
 
 export interface StructureGeneLinks {
-- 
GitLab