<script setup lang="ts"> 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 { useStructuresBasket } from '~/stores/structuresBasket'; interface Props { genes: StructureOperonGeneWithImg[] | null } const structureBasket = useStructuresBasket() const props = withDefaults(defineProps<Props>(), { genes: null }); const height = ref<number>(200) const svgRef = ref<SVGElement | null>(null) const margin = ref<PlotMargin>({ marginTop: 50, marginRight: 7, marginBottom: 1, marginLeft: 7, }) 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>(5) const totalGeneLength = computed(() => { const genes = toValue(computedGenes) return genes.reduce((acc, curr) => { return acc + (curr?.size ?? 10) + innerPadding.value }, 0) }) const domainGenes = computed(() => { return [0, totalGeneLength.value] }) const xScale = computed(() => { return d3.scaleBand() .paddingInner(0) .domain(toValue(domain)) .range([0, computedPlotWidth.value]) }) const xScaleGenes = computed(() => { return d3.scaleLinear() .domain(toValue(domainGenes)) .range([0, computedPlotWidth.value]) }) const yScale = ref(d3.scaleBand() .domain(['img', 'buff', 'buff2', 'gene']) .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 }) const computedPlotWidth = computed(() => { const { marginLeft, marginRight } = toValue(margin) return computedContainerWidth.value - marginLeft - marginRight }) const computedGenes = computed<StructureOperonGene[]>(() => { const genes = toValue(props.genes) if (genes !== null) { let currentSumSize = 0 return genes.map(d => { const size = d?.size ?? 10 const position = currentSumSize currentSumSize = position + size + innerPadding.value return { ...d, size, position } }) } else { return [] } }) const structureVersion = computed(() => { const genesVal = toValue(computedGenes) if (genesVal?.length > 0) { return genesVal[0].version } else { return undefined } }) const genesWithCoord = computed<StructureOperonGeneWithCoordinate[]>(() => { const genes = toValue(computedGenes) const xScaleVal = toValue(xScaleGenes) const yScaleVal = toValue(yScale) if (genes !== null) { return genes.map(d => { return { ...d, width: xScaleVal(d.size), x: xScaleVal(d.position), y: yScaleVal('gene'), height: yScaleVal.bandwidth() } }) } else { return [] } }) onMounted(() => { draw() }) watch(genesWithCoord, () => { draw() }) function createOrSelect(container: d3.Selection<SVGElement, any, HTMLElement | null, any>, tag: string, selectionClass: string) { let selection = container.select(`${tag}.${selectionClass}`) if (selection.empty()) { selection = container.append(tag).classed(selectionClass, true) } return selection } function draw() { if (svgRef.value !== null) { const svg = d3.select<SVGElement, undefined>(svgRef.value); const { marginLeft, marginTop } = toValue(margin) const xAxis = d3.axisTop(xScale.value) const gx = createOrSelect(svg, 'g', 'x-axis') gx .attr("transform", `translate(${marginLeft},${marginTop})`) .call(xAxis) gx.call(g => g.select(".domain") .remove()) .selectAll("text") .attr("transform", 'rotate(20)') .attr("text-anchor", "start") let gGenes = createOrSelect(svg, "g", "genes") gGenes .attr("transform", `translate(${marginLeft},0)`) .call(drawGenes, xScale, yScale) } } 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); // gene grp const gGene = gOperonItem.append("g") .classed("gene-grp", true) gGene .append("path") .classed("gene", true) // img group gOperonItem .append("g").classed("img", true) .append("image") .on("mouseover", function (event) { const target = d3.select(event.srcElement.parentElement) 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") }) gOperonItem.append("text") // .attr("fill", "white") .classed("gene-label", true) .attr("fill", "currentColor") .attr("dominant-baseline", "middle") gOperonItem.append("line") gOperonItem.append("title") return gOperonItem }, update => update, 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") .attr("transform", d => `translate(0, ${d.y})`) .attr("fill", d => 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() return context } </script> <template> <div ref="gbContainer"> <v-card flat color="transparent"> <v-card-item> <v-card-title>Operon structure <v-btn size="x-small" density="comfortable" variant="tonal" icon="mdi-help" @click="snackbar = true"> </v-btn></v-card-title> <v-card-subtitle>defenseFinder model version : {{ structureVersion }}</v-card-subtitle> </v-card-item> <svg ref="svgRef" :width="computedContainerWidth" :height="plotHeight"> <g class="x-axis" /> </svg> <v-snackbar v-model="snackbar"> Click on the structure image to visualize it. <template v-slot:actions> <v-btn color="info" variant="text" @click="snackbar = false"> Close </v-btn> </template> </v-snackbar> </v-card> </div> </template>