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