Skip to content
Snippets Groups Projects
[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} &rarr;`)
  }
  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>