diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue index 21e0969185d05701d96b534c5960ded3f0005ba5..a1a31799f6c096773c8c57fe84dbbd4cc59db58f 100644 --- a/components/OperonStructure.vue +++ b/components/OperonStructure.vue @@ -4,6 +4,7 @@ import * as d3 from "d3"; import type { PlotMargin } from "../types/plot"; import type { StructureGeneLinks, StructureOperonGene, StructureOperonGeneWithCoordinate, StructureOperonGeneWithImg } from "../types/structure" import { useStructuresBasket } from '~/stores/structuresBasket'; +import { useTextRotate, type TextRotateOuput } from '~/composables/useTextRotate'; interface Props { @@ -17,29 +18,25 @@ const props = withDefaults(defineProps<Props>(), { const { genes: genesProps } = toRefs(props) // const refGenes = ref() -const height = ref<number>(200) +// const height = ref<number>(200) const svgRef = ref<SVGElement | null>(null) +const structureHeight = ref(130) +const geneHeight = ref(40) +const linkHeight = ref(20) const margin = ref<PlotMargin>({ marginTop: 5, marginRight: 7, - marginBottom: 50, + marginBottom: 5, marginLeft: 7, }) - +const gbContainer = ref(null) const geneToHighlight = ref<string | null>(null) - const snackbar = ref(false) const color = d3.scaleOrdinal(d3.schemeCategory10); -const plotHeight = computed(() => { - const { marginTop, marginBottom } = toValue(margin) - return toValue(height) + marginTop + marginBottom -}) - const domain = computed(() => { const genes = toValue(computedGenes) return genes?.map(d => { return d.gene }) }) - const innerPadding = ref<number>(8) const totalGeneLength = computed(() => { @@ -66,12 +63,6 @@ const xScaleGenes = computed(() => { .domain(toValue(domainGenes)) .range([0, computedPlotWidth.value]) }) - -const yScale = ref(d3.scaleBand() - .paddingInner(0.5) - .domain(['img', 'buff', 'buff2', 'gene', 'label']) - .range([toValue(margin).marginTop, toValue(height)])); -const gbContainer = ref(null) const computedContainerWidth = computed(() => { return useElementSize(gbContainer, { width: 500, height: 0 }, { box: 'border-box' }).width.value }) @@ -80,13 +71,11 @@ const computedPlotWidth = computed(() => { return computedContainerWidth.value - marginLeft - marginRight }) - - const computedGenes = computed<StructureOperonGene[]>(() => { const genes = toValue(genesProps) if (genes !== null && genes?.length > 0) { let currentSumSize = 0 - return genes.map(d => { + return genes.map((d) => { const size = d?.size ?? 10 const position = currentSumSize currentSumSize = position + size + innerPadding.value @@ -100,9 +89,8 @@ const computedGenes = computed<StructureOperonGene[]>(() => { } else { return [] } - - }) + const structureVersion = computed(() => { const genesVal = toValue(computedGenes) if (genesVal?.length > 0) { @@ -113,57 +101,101 @@ const structureVersion = computed(() => { } }) -const structureNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { +const geneNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { const genes = toValue(computedGenes) - const xScaleVal = toValue(xScale) - const yScaleVal = toValue(yScale) + const xScaleVal = toValue(xScaleGenes) + // 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 - const x = xScaleVal(d.gene) - const y = yScaleVal('img') + const x = xScaleVal(d.position) + // const y = yScaleVal('gene') + const width = xScaleVal(d.size) + // let rotate = 0 + // let labelWidth = (d.gene.length * textSizeRatio.value) / Math.sqrt(2) + // let labelHeight = labelWidth + // if (width < labelWidth) { + // // need to increase angle + // rotate = 90 + // labelHeight = labelWidth + // labelWidth = 16 + // } + return { + ...d, + width, + x, + // rotate: 0, + // labelWidth, + labelHeight: 150, + y: 0, + // y: y === undefined ? 0 : y, + height: 0 + // height: yScaleVal.bandwidth() + } + }) + } + else { return [] } +}) + +const yScale = computed(() => { + const { marginTop } = toValue(margin) + const structureHeightVal = toValue(structureHeight) + marginTop + const linkHeightVal = toValue(linkHeight) + const geneHeightVal = toValue(geneHeight) + return { + img: 0, + gene: structureHeightVal + linkHeightVal, + label: structureHeightVal + linkHeightVal + geneHeightVal + 10 + } +}) + +const geneNodesWithY = computed(() => { + const genes = toValue(geneNodes) + const yScaleVal = toValue(yScale) + if (genes !== null) { + const y = yScaleVal.gene + return genes.map(d => { return { ...d, - r: r, - // x: xPostion > 0 ? xPostion : 0, - x: x === undefined ? 0 : x, y: y === undefined ? 0 : y, - width: toValue(xScale).bandwidth(), - // width: r, - height: yScaleVal.step() * 3 + height: geneHeight.value } }) } else { return [] } }) - -const geneNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { - const genes = toValue(computedGenes) - const xScaleVal = toValue(xScaleGenes) +const structureNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { + const genes = toValue(geneNodesWithY) + const xScaleVal = toValue(xScale) const yScaleVal = toValue(yScale) if (genes !== null) { return genes.map(d => { - const x = xScaleVal(d.position) - const y = yScaleVal('gene') + const x = xScaleVal(d.gene) + const y = yScaleVal.img return { ...d, - width: xScaleVal(d.size), - x, + x: x === undefined ? 0 : x, y: y === undefined ? 0 : y, - height: yScaleVal.bandwidth() + width: toValue(xScale).bandwidth(), + height: structureHeight.value } }) } else { return [] } }) +const geneLabelHeight = computed(() => { + const heights = toValue(genesTextRotate) + const max = d3.max(Object.values(heights).map(({ height }) => toValue(height))) + return max !== undefined ? max : 0 +}) + +const plotHeight = computed(() => { + const { marginTop, marginBottom } = toValue(margin) + return marginTop + marginBottom + toValue(geneLabelHeight) + toValue(structureHeight) + toValue(geneHeight) + toValue(linkHeight) + 10 +}) const linksGenesStruct = computed<StructureGeneLinks[]>(() => { - const geneNodesVal = toValue(geneNodes) + const geneNodesVal = toValue(geneNodesWithY) const structureNodesVal = toValue(structureNodes) return d3.zip(geneNodesVal, structureNodesVal).map(([source, target]) => { @@ -176,26 +208,30 @@ const linksGenesStruct = computed<StructureGeneLinks[]>(() => { }) - const genesLabel = computed(() => { - const geneNodesVal = toValue(geneNodes) + const geneNodesVal = toValue(geneNodesWithY) const yScaleVal = toValue(yScale) - - return geneNodesVal.map(d => ({ ...d, y: yScaleVal("label") })) + return geneNodesVal.map(d => ({ ...d, y: yScaleVal.label })) }) onMounted(() => { - // const genePropsVal = toValue(genesProps) - // refGenes.value = genePropsVal?.map(d => d) draw() }) +const genesTextRotate = ref<Record<string, TextRotateOuput>>({}) +watch(genesLabel, (newGenesLabel) => { + const genesTextRotateVal = toValue(genesTextRotate) + newGenesLabel.forEach(el => { + const textRotate = useTextRotate({ availableWidth: el.width }) + genesTextRotateVal[el.id.toString()] = textRotate + }) +}) watch(structureNodes, () => { draw() }) -watch(geneNodes, () => { +watch(geneNodesWithY, () => { draw() }) @@ -285,7 +321,7 @@ function drawStructure(operonGroup: d3.Selection<SVGGElement, any, SVGElement | exit => exit.remove() ) const imageSelection = structureSelection - .attr("transform", `translate(0,${toValue(yScale)("img")})`) + .attr("transform", `translate(0,${toValue(yScale).img})`) .attr("cursor", d => d.highlight ? "pointer" : null) .select("image") imageSelection @@ -302,7 +338,7 @@ function drawStructure(operonGroup: d3.Selection<SVGGElement, any, SVGElement | } function drawGenes(operonGroup: d3.Selection<SVGGElement, any, SVGElement | null, any>) { - const genesWithCoordVal = toValue(geneNodes) + const genesWithCoordVal = toValue(geneNodesWithY) const genes = genesWithCoordVal const genesSelection = operonGroup .selectAll("g.gene-grp") // get all "existing" lines in svg @@ -343,13 +379,12 @@ function drawGenes(operonGroup: d3.Selection<SVGGElement, any, SVGElement | null ) genesSelection .attr("cursor", d => d.highlight ? "pointer" : null) - .attr("transform", d => `translate(${d.x}, 0)`) + .attr("transform", d => `translate(${d.x}, ${d.y})`) const genePathSelection = genesSelection .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})`) genePathSelection .transition() .attr("fill", d => d?.highlight ? d3.color(color(d.system))?.brighter() : color(d.system)) @@ -412,15 +447,30 @@ function drawGenesLabel(operonGroup: d3.Selection<SVGGElement, any, SVGElement | ) updateSelection + .attr("cursor", d => d.highlight ? "pointer" : null) - .select("text") - .attr("transform", d => `translate(${d.x + d.width / 2},${d.y}) rotate(40) `) + .select<SVGTextElement>("text") + .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 + 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) updateSelection.select("title").text(d => d.gene) } + + + function adjacentlinks(nodes: Record<string, any>) { const links = [] for (let i = 0; i < nodes.length; i++) { diff --git a/composables/useTextRotate.ts b/composables/useTextRotate.ts new file mode 100644 index 0000000000000000000000000000000000000000..107f215b5f37422c9d4885bd607bc6606214dffd --- /dev/null +++ b/composables/useTextRotate.ts @@ -0,0 +1,77 @@ +import { useElementSize } from '@vueuse/core' + + +export interface TextRotateInput { + availableWidth: MaybeRef<number> +} + +export interface TextRotateOuput { + rotate: ComputedRef<number> + width: Ref<number> + height: Ref<number> + textWidth: ComputedRef<number>, + setTextElem: (newTextElement: MaybeRef<SVGTextElement>) => void +} + +export function useTextRotate({ availableWidth }: TextRotateInput): TextRotateOuput { + const width = ref<number>(0) + const height = ref<number>(20) + const textElement = ref<SVGTextElement | undefined>(undefined) + const availableWidthVal = toValue(availableWidth) + const textWidth = computed(() => { + const textElementVal = toValue(textElement) + if (textElementVal !== undefined) { + const { width } = textElementVal.getBBox() + return width + } + return 0 + }) + function setTextElem(newTextElement: MaybeRef<SVGTextElement>) { + textElement.value = toValue(newTextElement) + } + + const rotateRad = computed(() => { + const textWidthSide = textWidth.value / Math.sqrt(2) + 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 + return Math.PI / 2 + } + }) + + 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 + } + }) + + return { rotate, width, height, textWidth, setTextElem } +} \ No newline at end of file diff --git a/types/structure.ts b/types/structure.ts index 13b724bb9909ab95bd1e56b2c37bb298db65ab33..b61ea45c66947d93408e401a74f575f8215a08b4 100644 --- a/types/structure.ts +++ b/types/structure.ts @@ -21,6 +21,9 @@ export interface StructureOperonGeneWithCoordinate extends StructureOperonGeneWi height: number x: number y: number + labelHeight: number, + // labelWidth: number, + // rotate: number } export interface StructureGeneLinks {