diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue index f696a8629d16a76c9fd48f21bca35803436ada16..3c723d88926ddeb002c40a5a8c4ad7a988e88afa 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 { StructureOperonGene, StructureOperonGeneWithCoordinate, StructureOperonGeneWithImg } from "../types/structure" +import type { StructureGeneLinks, StructureOperonGene, StructureOperonGeneWithCoordinate, StructureOperonGeneWithImg } from "../types/structure" import { useStructuresBasket } from '~/stores/structuresBasket'; @@ -69,7 +69,7 @@ const xScaleGenes = computed(() => { const yScale = ref(d3.scaleBand() .paddingInner(0.5) - .domain(['img', 'buff', 'buff2', 'gene']) + .domain(['img', 'buff', 'buff2', 'gene', 'label']) .range([toValue(margin).marginTop, toValue(height)])); const gbContainer = ref(null) const computedContainerWidth = computed(() => { @@ -124,12 +124,15 @@ const structureNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { // 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') + return { ...d, r: r, // x: xPostion > 0 ? xPostion : 0, - x: xScaleVal(d.gene), - y: yScaleVal('img'), + x: x === undefined ? 0 : x, + y: y === undefined ? 0 : y, width: toValue(xScale).bandwidth(), // width: r, height: yScaleVal.step() * 3 @@ -145,11 +148,13 @@ const geneNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { const yScaleVal = toValue(yScale) if (genes !== null) { return genes.map(d => { + const x = xScaleVal(d.position) + const y = yScaleVal('gene') return { ...d, width: xScaleVal(d.size), - x: xScaleVal(d.position), - y: yScaleVal('gene'), + x, + y: y === undefined ? 0 : y, height: yScaleVal.bandwidth() } }) @@ -157,7 +162,7 @@ const geneNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { else { return [] } }) -const linksGenesStruct = computed(() => { +const linksGenesStruct = computed<StructureGeneLinks[]>(() => { const geneNodesVal = toValue(geneNodes) const structureNodesVal = toValue(structureNodes) @@ -165,13 +170,20 @@ const linksGenesStruct = computed(() => { 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 + highlight: source?.highlight || target?.highlight || false } }) }) +const genesLabel = computed(() => { + const geneNodesVal = toValue(geneNodes) + const yScaleVal = toValue(yScale) + + return geneNodesVal.map(d => ({ ...d, y: yScaleVal("label") })) +}) + onMounted(() => { // const genePropsVal = toValue(genesProps) // refGenes.value = genePropsVal?.map(d => d) @@ -187,57 +199,57 @@ watch(geneNodes, () => { draw() }) -function createOrSelect(container: d3.Selection<SVGElement, any, HTMLElement | null, any>, tag: string, selectionClass: string) { - let selection = container.select(`${tag}.${selectionClass}`) +/** + * create or select within a container a svg element and return it + * @param container + * @param tag + * @param selectionClass + */ +function createOrSelect<Type extends d3.BaseType, ParentType extends d3.BaseType>(container: d3.Selection<ParentType, any, SVGElement | null, any>, tag: string, selectionClass: string) { + let selection = container.select<Type>(`${tag}.${selectionClass}`) if (selection.empty()) { - selection = container.append(tag).classed(selectionClass, true) + selection = container.append<Type>(tag).classed(selectionClass, true) } return selection } + + function draw() { if (svgRef.value !== null) { - const svg = d3.select<SVGElement, undefined>(svgRef.value); + const svg = d3.select<SVGElement, any>(svgRef.value); const { marginLeft, marginTop } = toValue(margin) - const xAxis = d3.axisBottom(xScaleGenes.value) - const gx = createOrSelect(svg, 'g', 'x-axis') - gx - .attr("transform", `translate(${marginLeft},${toValue(height) + marginTop})`) - .call(xAxis) - - gx.call(g => g.select(".domain") - .remove()) - .selectAll("text") - .attr("transform", 'rotate(20)') - .attr("text-anchor", "start") - - let gOperon = createOrSelect(svg, "g", "operon") + // const xAxis = d3.axisBottom(xScaleGenes.value) + const gx = createOrSelect<SVGGElement, SVGElement>(svg, 'g', 'x-axis') + // gx + // .attr("transform", `translate(${marginLeft},${toValue(height) + marginTop})`) + // .call(xAxis) + + // gx.call(g => g.select(".domain") + // .remove()) + // .selectAll("text") + // .attr("transform", 'rotate(20)') + // .attr("text-anchor", "start") + + let gOperon = createOrSelect<SVGGElement, SVGElement>(svg, "g", "operon") gOperon .attr("transform", `translate(${marginLeft},0)`) .call(drawLinks) .call(drawGenes) .call(drawStructure) + .call(drawGenesLabel) } } -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 -} -function drawLinks(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) { - const stroke = "#555" // stroke for links + +function drawLinks(operonGroup: d3.Selection<SVGGElement, any, SVGElement | null, any>) { const strokeWidth = 1.5 // stroke width for links const strokeOpacity = 0.4 // stroke opacity for links const updateSelection = operonGroup - .selectAll("path.link") + .selectAll<SVGPathElement, StructureGeneLinks>("path.link") .data(linksGenesStruct.value) .join("path") .classed("link", true) @@ -252,7 +264,7 @@ function drawLinks(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) .attr("d", d3.link(d3.curveBumpY)) } -function drawStructure(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) { +function drawStructure(operonGroup: d3.Selection<SVGGElement, any, SVGElement | null, any>) { const structureNodeVal = toValue(structureNodes) // const totalSize = domainGenes.value[1] @@ -276,19 +288,12 @@ function drawStructure(operonGroup: d3.Selection<SVGElement, any, SVGElement, an gStructure .append("image") .on("mouseover", function (event) { - const srcSelection = d3.select(event.srcElement) - const target = d3.select(event.srcElement.parentElement) + const srcSelection = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(event.srcElement) const node = srcSelection.data() geneToHighlight.value = node[0].gene - - target - .attr("cursor", "pointer") }) .on("mouseout", function (event) { - const target = d3.select(event.srcElement.parentElement) - target - .attr("cursor", "unset") geneToHighlight.value = null }) @@ -299,7 +304,8 @@ function drawStructure(operonGroup: d3.Selection<SVGElement, any, SVGElement, an exit => exit.remove() ) const imageSelection = structureSelection - .attr("transform", d => `translate(0,${toValue(yScale)("img")})`) + .attr("transform", `translate(0,${toValue(yScale)("img")})`) + .attr("cursor", d => d.highlight ? "pointer" : null) .select("image") imageSelection .transition() @@ -318,22 +324,26 @@ function drawStructure(operonGroup: d3.Selection<SVGElement, any, SVGElement, an } - -function drawGenes(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) { +function drawGenes(operonGroup: d3.Selection<SVGGElement, any, SVGElement | null, 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); + .classed("operon-item", true) + .on("mouseover", function (event) { + const srcSelection = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(event.srcElement) + const node = srcSelection.data() + geneToHighlight.value = node[0].gene + }) + + .on("mouseout", function (event) { + geneToHighlight.value = null + }) // gene grp const gGene = gOperonItem.append("g") .classed("gene-grp", true) @@ -380,11 +390,58 @@ function drawGenes(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) context.lineTo(width, height) context.lineTo(0, height) context.closePath() - return context } } +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) + .join( + enter => { + const labelSelection = enter + .append("g") + .classed("gene-label", true) + .on("mouseover", function (event) { + const srcSelection = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(event.srcElement) + const node = srcSelection.data() + geneToHighlight.value = node[0].gene + }) + + .on("mouseout", function (event) { + geneToHighlight.value = null + + }) + labelSelection + .append("text") + .attr("fill", "currentColor") + .attr("dominant-baseline", "middle") + labelSelection.append("title") + return labelSelection + }, + update => update, + exit => exit.remove() + ) + + updateSelection.select("text") + .attr("transform", d => `translate(${d.x + d.width / 2},${d.y}) rotate(40) `) + .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++) { + if (i < nodes.length - 1) { + links.push({ index: i, source: nodes[i], target: nodes[+1] }) + } + } + return links +} </script> <template> <div ref="gbContainer"> diff --git a/types/structure.ts b/types/structure.ts index 2f5997a3d19a87d839e9aa6b05f79f9e2f113a34..13b724bb9909ab95bd1e56b2c37bb298db65ab33 100644 --- a/types/structure.ts +++ b/types/structure.ts @@ -19,9 +19,17 @@ export interface StructureOperonGeneWithImg extends StructureOperonGene { export interface StructureOperonGeneWithCoordinate extends StructureOperonGeneWithImg { width: number height: number - x: number | undefined - y: number | undefined + x: number + y: number } + +export interface StructureGeneLinks { + source: [number, number] + target: [number, number] + highlight: boolean +} + + export type StructurePredictionType = "monomer" | "multimer" | "multimer(dimer)" | "multimer(homodimer)" export type StructureType = "Validated" | "DF" | "na"