-
Remi PLANEL authoredRemi PLANEL authored
[analysisId].vue 12.74 KiB
<script setup lang="ts">
import { useThrottleFn } from '@vueuse/core'
import { useFetchAnalysis } from "../../composables/useFetchAnalysis";
import { useSelectedProtein } from '@/composables/useSelectedProtein'
import { useRoute, computed } from "#imports";
import * as d3 from "d3";
import { useDisplay } from 'vuetify'
import { useElementSize } from '@vueuse/core'
const { width } = useDisplay()
const route = useRoute();
const { selectedProtein } = useSelectedProtein()
const { data: analysis } = await useFetchAnalysis(
route.params.analysisId,
5000,
true
);
useHead({
title: analysis.value?.name ?? 'analysis',
})
const breadcrumbItems = computed(() => {
return [
{
title: 'Home',
disabled: false,
to: { name: 'index' }
},
{
title: 'Analyses',
disabled: false,
to: { name: 'analyses' },
},
{
title: analysis.value?.name,
disabled: true,
to: {
name: route.name, params: { ...route.params },
}
}
]
})
const { data: rawProteins, error } = await useAPI<ProteinsOut>(
`/analysis/${route.params.analysisId}/proteins`
);
const { data: genes } = await useAPI<GenesOut>(
`/analysis/${route.params.analysisId}/genes`
);
const { data: rawHmmer } = await useAPI<HmmersOut>(
`/analysis/${route.params.analysisId}/hmmers`
);
const height = ref(300)
const genesMap = computed(() => {
const mapGenes = new Map<string, Gene>()
const toValGenes = toValue(genes)
if (toValGenes !== null) {
for (const gene of toValGenes.genes) {
mapGenes.set(gene.hit_id, { ...gene, hit_gene_ref: gene.hit_gene_ref.split('__')[0].toLowerCase() })
}
}
return mapGenes
})
const hmmersMap = computed(() => {
const hmmMap = new Map<string, Hmmer>()
const toValHmmers = toValue(rawHmmer)
if (toValHmmers !== null) {
for (const hmmer of toValHmmers.hmmers) {
if (hmmMap.has(hmmer.hit_id)) {
const hmmerInMapScore = hmmMap.get(hmmer.hit_id)?.hit_score ?? -1
if (hmmerInMapScore < hmmer.hit_score) {
hmmMap.set(hmmer.hit_id, { ...hmmer })
}
} else {
hmmMap.set(hmmer.hit_id, { ...hmmer })
}
}
}
return hmmMap
})
const computedXUnit = computed(() => {
if (rawProteins.value?.proteins) {
if (rawProteins.value.proteins[0].start === null && rawProteins.value.proteins[0].end === null) {
return "aa"
}
else { return "bp" }
}
})
const sanitizedData = computed(() => {
if (rawProteins.value?.proteins) {
const toValGenesMap = toValue(genesMap)
const toValHmmerMap = toValue(hmmersMap)
const prots = rawProteins.value.proteins.map(prot => {
const isHmmerHit = toValHmmerMap.has(prot.id)
const isDefenseSystem = toValGenesMap.has(prot.id)
return {
...prot,
name: prot.id,
gene_name: toValHmmerMap.get(prot.id)?.gene_name,
strand: prot.strand,
defenseSystem: toValGenesMap.get(prot.id)?.hit_gene_ref,
isDefenseSystem,
isHmmerHit
}
})
const proteins = []
let currentGenomeIndex = 1
for (const prot of prots) {
if (prot?.start !== null && prot?.end !== null) {
proteins.push({ ...prot, length: prot.end - prot.start, protLength: prot.length })
}
else {
proteins.push({ ...prot, start: currentGenomeIndex, end: currentGenomeIndex + prot.length - 1, protLength: prot.length })
currentGenomeIndex = currentGenomeIndex + prot.length + 20
}
}
return proteins
}
else { return [] }
})
const proteinIndex = computed(() => {
return new Map(sanitizedData.value.map((d, i, arr) => {
let previousStart = 0
if (i > 0) {
previousStart = arr[i - 1].start - 10
}
let nextEnd = previousStart + 400
if (i < arr.length - 1) {
nextEnd = arr[i + 1].end + 10
}
return [d.id, { ...d, previousStart, nextEnd }]
}))
})
const displayHmmerHits = ref<boolean>(true)
type ColorScale = (value: string | number) => string
const color = computed<ColorScale>(() => {
return d3.scaleOrdinal(["default", undefined, "df", "hmmer"], d3.schemeTableau10)
})
const gbContainer = ref(null)
const gbContainerWidth = ref(useElementSize(gbContainer, { width: 500, height: 0 }, { box: 'border-box' }).width)
const computedWidth = computed(() => {
return gbContainerWidth.value
})
watch(selectedProtein, () => {
const toValHitId = toValue(selectedProtein)
const toValProtIndex = toValue(proteinIndex)
if (toValHitId && toValProtIndex.size > 0 && toValProtIndex.has(toValHitId)) {
const prot = toValProtIndex.get(toValHitId)
domain.value = [prot?.previousStart ?? 0, prot?.nextEnd ?? 400]
}
})
const marginLeftGb = ref(10)
const marginRightGb = ref(10)
const marginBottomGb = ref(10)
const innerWidth = computed(() => {
return computedWidth.value - marginLeftGb.value - marginRightGb.value
})
const innerHeigth = computed(() => {
return height.value - marginBottomGb.value
})
const minRange = ref(0)
const maxRange = ref(innerWidth.value)
const svgRef = ref(null)
const domain = ref([0, 10000])
// const range = ref()
const xScale = ref(d3.scaleLinear()
.domain(domain.value)
.range([minRange.value, maxRange.value])
);
const yScale = ref(d3.scaleLinear()
.domain([-1, 1])
.range([0, innerHeigth.value]));
const computedData = computed(() => {
const newData = sanitizedData.value.filter(gene => {
const { start, end } = gene
const [scaleStart, scaleEnd] = xScale.value.domain()
return start <= scaleEnd && end >= scaleStart
}).map(gene => {
const width = xScale.value(gene.end) - xScale.value(gene.start)
const height = yScale.value(-0.75)
const x = xScale.value(gene.start) + marginLeftGb.value
const y = yScale.value(-0.75) - marginBottomGb.value
return {
...gene,
width,
height,
x,
y
}
})
return newData
})
function drawGene({ width, height, strand }) {
const context = d3.path()
const halfHeight = height / 2
const isWidthLonger = halfHeight < width
if (strand < 0) {
context.moveTo(0, halfHeight)
if (isWidthLonger) context.lineTo(halfHeight, 0)
context.lineTo(width, 0)
context.lineTo(width, height)
if (isWidthLonger) context.lineTo(halfHeight, height)
context.closePath()
} else if (strand > 0) {
context.moveTo(0, 0)
if (isWidthLonger) context.lineTo(width - halfHeight, 0)
context.lineTo(width, halfHeight)
if (isWidthLonger) context.lineTo(width - halfHeight, height)
context.lineTo(0, height)
context.closePath()
}
else {
context.moveTo(0, 0)
context.lineTo(width, 0)
context.lineTo(width, height)
context.lineTo(0, height)
context.closePath()
}
return context
}
function positionText(selection) {
selection.each(function (d) {
const textWidth = this.clientWidth
if (d.width < 10) {
d3.select(this)
.text('')
}
// else {
const halfW = d.width / 2
const halfTw = textWidth / 2
const k = d.height / 8
const x = d.strand > 0 ? halfW - halfTw - k : halfW - halfTw + k
d3.select(this)
.attr("transform", `translate(${halfW},45) rotate(45)`)
// }
})
}
function proteinText(item) {
if (item.isHmmerHit && displayHmmerHits.value || item.isDefenseSystem) {
return `${item.gene_name} / ${item.name}`
}
}
function proteinTitle(item) {
let title = `name=${item.name} | length=${item.length}`
if (item.isHmmerHit && !item.isDefenseSystem) {
title = `gene name=${item.gene_name} | ${title}`
}
if (item.isDefenseSystem) {
title = `system=${item.defenseSystem} | ${title}`
}
return title
}
const maxLabelSize = ref(38)
function proteinTextTrunc(item) {
const text = proteinText(item)
return (text && text.length > maxLabelSize.value) ? text.slice(0, maxLabelSize.value - 1) + '...' : text;
}
function drawGenes(genesSelection) {
genesSelection
.selectAll("g.gene") // get all "existing" lines in svg
.data(computedData.value) // sync them with our data
.join(
enter => {
const g = enter.append("g")
.classed("gene", true);
g.append("path").classed("gene", true)
g.append("text")
// .attr("fill", "white")
.classed("gene-label", true)
.attr("fill", "currentColor")
.attr("dominant-baseline", "middle")
g.append("title")
},
update => {
update
.attr("transform", d => `translate(${d.x},${d.y})`)
update.select("path.gene")
.attr("d", d => drawGene(d).toString())
.attr("fill", d => {
if (d.isDefenseSystem) return toValue(color)("df")
if (displayHmmerHits.value && d.isHmmerHit) return toValue(color)("hmmer")
return toValue(color)("default")
})
update.select("text.gene-label").text(proteinTextTrunc).call(positionText)
update.select("title").text(proteinTitle)
},
exit => exit.remove()
)
}
function draw() {
const throttledZoomed = useThrottleFn((event) => {
zoomed(event)
}, 50)
function zoomed(event) {
const { transform } = event
const zx = transform.rescaleX(xScale.value);
domain.value = zx.domain()
minRange.value = zx.range()[0]
maxRange.value = zx.range()[1]
}
const svg = d3.select(svgRef.value);
const zoom = d3.zoom()
.scaleExtent([0.5, 32])
.on("zoom", throttledZoomed);
const xAxis = d3.axisBottom(xScale.value)
let gx = svg.select("g.xaxis")
if (gx.empty()) {
gx = svg.append("g").classed("xaxis", true)
}
gx
.attr("transform", `translate(${marginLeftGb.value},${height.value - 18})`)
.call(xAxis)
let gxTitle = gx.select("text.x-axis-title")
if (gxTitle.empty()) {
gxTitle = gx.append("text")
.classed("x-axis-title", true)
.attr("text-anchor", "end")
.attr("fill", "currentColor")
.html(() => `${computedXUnit.value} →`)
}
gxTitle
.attr("x", innerWidth.value)
.attr("y", - 10)
let gGenes = svg.select("g.genes")
if (gGenes.empty()) {
gGenes = svg.append("g").classed("genes", true)
}
gGenes.call(drawGenes, xScale, yScale)
svg.call(zoom).call(zoom.transform, d3.zoomIdentity);
}
onMounted(() => {
draw()
})
watchEffect(() => {
xScale.value = d3.scaleLinear()
.domain(domain.value) // input values...
.range([minRange.value, innerWidth.value])
draw()
})
const selectedResult = ref(null);
</script>
<template>
<v-card v-if="analysis" flat color="transparent">
<v-app-bar color="background">
<v-breadcrumbs :items="breadcrumbItems"></v-breadcrumbs>
</v-app-bar>
<v-card>
<v-toolbar density="compact" class="pr-2">
<v-toolbar-title>{{ analysis.name }}</v-toolbar-title>
<v-btn color="primary" prepend-icon="mdi-download"
:href="`/api/analysis/${analysis.id}/results-archive`">Download
all results</v-btn>
<v-chip color="primary" rounded>{{ new Date(analysis.create_time).toLocaleString() }}</v-chip>
<template v-if="analysis.percentage_done !== 100 && analysis.stderr === ''" #extension>
<v-row>
<v-col cols="12">
<v-card flat color="transparent">
<v-card-text>
<v-progress-linear indeterminate color="primary"></v-progress-linear>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
</v-toolbar>
<template v-if="analysis.stderr !== ''">
<v-card color="error" variant="tonal" class="my-2">
<v-card-title>Standard error</v-card-title>
<v-card-text>
<pre> {{ analysis.stderr }} </pre>
</v-card-text>
</v-card>
</template>
<template v-else>
<v-card-text>
<div ref="gbContainer">
<v-card flat color="transparent">
<v-toolbar variant="flat" density="compact" color="transparent">
<v-spacer></v-spacer><v-toolbar-items>
<v-switch v-model="displayHmmerHits" color="primary" label="Display HMM-only hits
" class="mr-2"></v-switch>
</v-toolbar-items></v-toolbar>
<svg ref="svgRef" :width="computedWidth" :height="height">
<g class="x-axis" />
</svg>
</v-card>
</div>
</v-card-text>
<v-card-text>
<v-btn-toggle v-model="selectedResult" rounded="0" color="primary" group>
<v-btn value="systems" :to="`/analyses/${analysis.id}/systems`">
Systems
</v-btn>
<v-btn value="genes" exact :to="`/analyses/${analysis.id}/genes`">
Genes
</v-btn>
<v-btn value="hmmer" :to="`/analyses/${analysis.id}/hmmer`">
Hmmer
</v-btn>
</v-btn-toggle>
</v-card-text>
</template>
<NuxtPage />
</v-card>
</v-card>
</template>