"git@gitlab.pasteur.fr:aperrin/pipeline_annotation.git" did not exist on "f67ea743b99d0d79739a9347c0ed12fac7281f12"
Select Git revision
SubmitAnalysis.vue
RefseqDb.vue 24.64 KiB
<script setup lang="ts">
import * as d3 from "d3";
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import { useDisplay } from "vuetify";
import { ServerDbTable } from "#components"
import { useSerialize } from "@/composables/useSerialize";
import { useRasterize } from "@/composables/useRasterize";
import { useDownloadBlob } from '@/composables/useDownloadBlob';
import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
import type { ComponentPublicInstance } from 'vue'
const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }])
const itemValue = ref("id");
const { width } = useDisplay();
const dbName = ref("refseq")
const taxonomyFacet = ref<Record<string, any> | undefined>(undefined)
const cellPlotMargin = ref({
marginLeft: 150,
marginBottom: 200,
marginTop: 0,
marginRight: 50
})
const client = useMeiliSearchRef()
onMounted(async () => {
try {
const data = await client.index(toValue(dbName)).search("", {
facets: ["*"],
filter: [],
page: 1,
hitsPerPage: 25,
})
autocompleteMeiliFacetsProps.value.facetDistribution = data?.facetDistribution
} catch (error) {
}
await getAllHits({
index: toValue(dbName), query: "", params: {
facets: ["*"],
filter: [],
limit: 500000,
sort: ["type:asc"]
}
})
try {
const taxo = await client.index("refseqtaxo").search("", {
facets: ["*"],
filter: [],
page: 1,
hitsPerPage: 25,
})
taxonomyFacet.value = taxo?.facetDistribution
} catch (error) {
}
})
const { serialize } = useSerialize()
const { rasterize } = useRasterize()
const { download } = useDownloadBlob()
const autocompleteMeiliFacetsProps = ref<AutocompleteMeiliFacetProps>({
db: toValue(dbName),
facets: [
{ title: "Defense System", type: "subheader" },
{ title: "System", value: "type", type: "facet", icon: "i-tabler:virus-off", },
{ title: "Subsystem", value: "subtype", type: "facet", icon: "i-tabler:virus-off" },
{ type: "divider" },
{ title: "Taxonomy", type: "subheader" },
{ title: "Superkingdom", value: "Superkingdom", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Phylum", value: "phylum", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Order", value: "order", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Family", value: "family", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Genus", value: "genus", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Species", value: "species", type: "facet", icon: "i-tabler:binary-tree" },
{ type: "divider" },
{ title: "Replicon", value: "replicon", type: "facet", icon: "mdi-dna", },
],
facetDistribution: undefined
})
const computedAutocompleteMeiliFacetsProps = computed(() => {
const toValFacetDistribution = toValue(autocompleteMeiliFacetsProps).facetDistribution
const toValFacets = toValue(autocompleteMeiliFacetsProps).facets
if (toValFacetDistribution !== undefined && toValFacets !== undefined) {
return {
...toValue(autocompleteMeiliFacetsProps), facets: toValFacets.map(facet => {
if (facet.type === "facet") {
const count = toValFacetDistribution?.[facet.value] ? Object.keys(toValFacetDistribution[facet.value]).length : undefined
return count ? { ...facet, count } : { ...facet }
}
else {
return { ...facet }
}
})
}
}
else {
return toValue(autocompleteMeiliFacetsProps)
}
})
const availableTaxo: Ref<string[]> = ref([
"species",
"genus",
"family",
"order",
"class",
"phylum",
"Superkingdom"
]);
const scaleTypes = ref<string[]>(['linear', 'sqrt', 'symlog'])
const selectedTaxoRank = ref<"species" | "genus" | "family" | "order" | "class" | "phylum" | "Superkingdom">("Superkingdom");
const headers = ref([
{
title: "System",
key: "type",
fixed: true
},
{ title: "Assembly", key: "Assembly" },
{ title: "Replicon", key: "replicon" },
{
title: "Subsystem",
key: "subtype",
},
{
title: "Accessions",
key: "accession_in_sys",
sortable: false
}
])
const fullWidth = computed(() => {
return layoutPlot.value === 'fullwidth'
})
const computedHeaders = computed(() => {
return [...headers.value, ...availableTaxo.value.map(taxo => {
return {
title: capitalize(taxo),
key: taxo
}
})]
})
const { result: msResult } = useMeiliSearch(toValue(dbName))
const computedWidth = computed(() => {
const currentWidth = fullWidth.value ? width.value : width.value / 2
return Math.max(currentWidth, 550);
});
const cellPlotComputedDimension = computed(() => {
const { marginLeft, marginBottom, marginRight, marginTop } = toValue(cellPlotMargin)
const toValWidth = toValue(width)
const widthFixCell = countSystem.value * 50 + marginLeft + marginRight
const heigthFix = countClade.value * 50 + marginTop + marginBottom
if (widthFixCell > toValWidth) {
return { width: toValWidth - marginLeft - marginRight, height: undefined }
} else {
return { width: widthFixCell, height: heigthFix }
}
})
const allHits: Ref<Record<string, any> | undefined> = ref(undefined)
const pendingAllHits = ref(false)
async function getAllHits(params: { index: string, params: Record<string, any>, query: string }) {
if (!params?.params?.filter || params?.params?.filter?.length === 0) {
selectedTaxoRank.value = "Superkingdom"
}
// if (params.index === toValue(dbName)) {
pendingAllHits.value = true
try {
const data = await client
.index("refseqsanitized")
.search("", {
...params.params,
'attributesToRetrieve': ['type', 'Assembly', ...toValue(availableTaxo)]
})
allHits.value = data
} finally {
pendingAllHits.value = false
}
// }
}
const defaultDataTableServerProps = ref({
showExpand: false
})
const dataTableServerProps = computed(() => {
return {
...defaultDataTableServerProps.value,
headers: computedHeaders.value,
itemValue: itemValue.value
}
})
const defaultBarPlotOptions = computed(() => {
return {
x: { label: null, tickRotate: 45 },
y: { grid: true, clamp: true },
// height: plotHeight.value + 100,
}
})
// system distri
const computedSystemDistribution = computed(() => {
if (toValue(msResult)?.facetDistribution?.type) {
return Object.entries(toValue(msResult).facetDistribution.type)
.map(([key, value]) => {
return {
type: key,
count: value
}
}).sort()
} else { return [] }
})
const computedDistriSystemOptions = computed(() => {
// const toValNormalizePerAssembly = toValue(normalizePerAssembly)
return {
...defaultBarPlotOptions.value,
title: "System distribution",
marginBottom: 100,
y: { ...defaultBarPlotOptions.value.y, type: toValue(scaleType), label: "Count" },
x: { ...defaultBarPlotOptions.value.x, label: "Systems" },
width: computedWidth.value,
marks: [
Plot.barY(
toValue(computedSystemDistribution),
{
y: "count", x: 'type', tip: true,
sort: { x: "-y" },
},
),
],
};
});
// Taxo distri
const computedTaxonomyDistribution = computed(() => {
const toValSelectedTaxoRank = toValue(selectedTaxoRank)
const toValFacetsPerRank = toValue(msResult)?.facetDistribution?.[toValSelectedTaxoRank]
if (toValFacetsPerRank) {
return Object.entries(toValFacetsPerRank).map(([key, value]) => {
return {
[toValSelectedTaxoRank]: key,
count: value
}
}).sort()
} else { return [] }
})
const computedDistriTaxoOptions = computed(() => {
return {
...defaultBarPlotOptions.value,
title: "Taxonomic distribution",
marginBottom: 100,
x: { ...defaultBarPlotOptions.value.x, label: selectedTaxoRank.value },
y: { ...defaultBarPlotOptions.value.y, type: toValue(scaleType), label: "Count" },
width: computedWidth.value,
marks: [
Plot.barY(
toValue(computedTaxonomyDistribution),
{
y: "count",
x: selectedTaxoRank.value,
tip: true,
// fill: "#6750a4",
sort: { x: "-y" },
}
),
],
};
});
function capitalize(name: string) {
const [first, ...rest] = name
return first.toUpperCase() + rest.join('').toLowerCase();
}
function namesToCollapsibleChips(names: string[]) {
return names.filter((it) => it !== "").map(it => ({ title: it }))
}
function namesToAccessionChips(names: string[]) {
return namesToCollapsibleChips(names).map(it => {
return {
...it,
href: new URL(it.title, "https://www.ncbi.nlm.nih.gov/protein/").toString()
}
})
}
const systemPanel: Ref<string[]> = ref([])
const layoutPlot: Ref<string> = ref("grid")
function compareBySystem(a, b) {
if (a === "No system found") {
return -1
}
if (b === "No system found") {
return 1
}
return a - b
}
const sortedCellDomain = computed(() => {
const toValueAllHits = toValue(allHits)
if (toValueAllHits && toValueAllHits?.hits?.length > 0) {
return toValueAllHits.hits.map(d => d.type).sort(compareBySystem)
}
})
const binPlotOptions = computed(() => {
const { marginLeft, marginBottom } = toValue(cellPlotMargin)
const { height } = toValue(cellPlotComputedDimension)
return {
marginLeft,
marginBottom,
padding: 0,
grid: true,
aspectRatio: height ? undefined : 1,
x: { tickRotate: 90, label: "Systems", domain: toValue(sortedCellDomain) },
color: { scheme: "plasma", legend: true, label: `Proportion per ${selectedTaxoRank.value}`, domain: [0, 100] },
}
})
const countSystem = computed(() => {
const toValueAllHits = toValue(allHits)
const data = toValueAllHits?.hits ?? []
const setSystem = new Set(data.map(d => d.type))
return setSystem.size
})
const countClade = computed(() => {
const toValueAllHits = toValue(allHits)
const data = toValueAllHits?.hits ?? []
const setSystem = new Set(data.map(d => d[selectedTaxoRank.value]))
return setSystem.size
})
const binPlotGroup = computed(() => {
return Plot.group(
{
label: (d) => d.fill,
fill: {
/**
*
* @param I is the list of element index that are par of the same group (cell)
* @param X is the list of all elements
*/
reduceIndex: function (I, X) {
const toValTaxonomyFacet = toValue(taxonomyFacet)
if (toValTaxonomyFacet !== undefined) {
const clade = X[I[0]][selectedTaxoRank.value]
const system = X[I[0]].type
// Get the list of all the items for this group (same cell)
// and group them per type and assembly
const itemsPerGroup = d3.rollup(I.map(index => {
return X[index]
}), D => D.length, d => d.type, d => d.Assembly)
const countForClade = toValTaxonomyFacet[selectedTaxoRank.value][clade]
const frequency = (itemsPerGroup.get(system).size / countForClade) * 100
return frequency
}
return I.length
}
},
},
{
x: "type",
y: selectedTaxoRank.value,
tip: true,
inset: 0.5,
sort: { y: "fill" }
}
)
})
const binPlotDataOptions = computed(() => {
const toValueAllHits = toValue(allHits)
const toValBinPlotGroup = toValue(binPlotGroup)
const data = toValueAllHits?.hits ?? []
const plotCellMark = Plot.cell(data, toValBinPlotGroup)
const { width, height } = toValue(cellPlotComputedDimension)
const dim = height ? { width, height } : { width }
return toValueAllHits?.hits?.length > 0 ? {
...binPlotOptions.value,
...dim,
title: `Proportion of genomes with defense system X per ${selectedTaxoRank.value} taxonomic rank`,
color: {
...binPlotOptions.value.color,
type: scaleType.value,
tickFormat: '~s',
ticks: scaleType.value === 'symlog' ? 3 : 5,
},
marks: [plotCellMark],
} : null
})
const scaleType = ref("linear")
const systemsDistributionPlot = ref<ComponentPublicInstance | null>(null)
const taxonomicDistributionPlot = ref<ComponentPublicInstance | null>(null)
const heatmapPlot = ref<ComponentPublicInstance | null>(null)
function downloadSvg(component: ComponentPublicInstance | null, filename: string) {
const blob = toValue(serialize(toValue(component)))
if (blob !== undefined) {
download(blob, filename)
}
}
async function downloadPng(component: ComponentPublicInstance | null, filename: string) {
const blob = await rasterize(toValue(component), filename)?.then((blob) => {
download(blob, filename)
})
}
</script>
<template>
<v-card flat class="mb-2" color="transparent">
<v-card color="transparent" flat>
<v-expansion-panels v-model="systemPanel" class="my-2" density="compact" multiple>
<v-expansion-panel elevation="3" value="barplot">
<v-expansion-panel-title color="grey-lighten-4"><v-icon color="primary"
class="mr-2">mdi-chart-bar</v-icon>Systems - Taxonomic</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card flat color="transparent">
<v-toolbar flat color="transparent" density="compact">
<v-btn-toggle v-model="layoutPlot" density="compact" rounded="false" variant="text"
color="primary" mandatory class="mx-2">
<v-btn icon="md:grid_view" value="grid"></v-btn>
<v-btn icon="md:view_agenda" value="fullwidth"></v-btn>
</v-btn-toggle>
<v-select v-model="scaleType" class="mx-2" density="compact" :items="scaleTypes"
label="Scale Type" hide-details="auto"></v-select>
<v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact"
label="Select taxonomic rank" hide-details="auto" class="mx-2"></v-select>
</v-toolbar>
<v-row align="start">
<v-col :cols="fullWidth ? 12 : 6">
<v-card variant="flat">
<v-toolbar density="compact" color="transparent">
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="md:download" v-bind="props">
export
</v-btn>
</template>
<v-list>
<v-list-item value="svg">
<v-list-item-title
@click="downloadSvg(systemsDistributionPlot, 'df-systems-distribution.svg')">to
svg</v-list-item-title>
</v-list-item>
<v-list-item value="png">
<v-list-item-title
@click="downloadPng(systemsDistributionPlot, 'df-systems-distribution.png')">to
png</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<PlotFigure ref="systemsDistributionPlot"
:options="unref(computedDistriSystemOptions)" defer></PlotFigure>
</v-card>
</v-col>
<v-col :cols="fullWidth ? 12 : 6">
<v-card variant="flat">
<v-toolbar density="compact" color="transparent">
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="md:download" v-bind="props">
export
</v-btn>
</template>
<v-list>
<v-list-item value="svg">
<v-list-item-title
@click="downloadSvg(taxonomicDistributionPlot, 'df-taxonomic-distribution.svg')">to
svg</v-list-item-title>
</v-list-item>
<v-list-item value="png">
<v-list-item-title
@click="downloadPng(taxonomicDistributionPlot, 'df-taxonomic-distribution.png')">to
png</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<PlotFigure ref="taxonomicDistributionPlot" defer
:options="unref(computedDistriTaxoOptions)"></PlotFigure>
</v-card>
</v-col>
</v-row>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel elevation="3" value="matrix">
<v-expansion-panel-title color="grey-lighten-4">
<v-icon v-if="pendingAllHits === false" color="primary" class="mr-2">mdi-data-matrix
</v-icon>
<v-progress-circular v-else indeterminate color="primary" :size="22" class="mr-2"
:width="3"></v-progress-circular>
Heatmap </v-expansion-panel-title>
<v-expansion-panel-text>
<v-card v-if="pendingAllHits === false" flat color="transparent">
<v-toolbar flat color="transparent" density="compact">
<v-select v-model="scaleType" class="mx-2" density="compact" :items="scaleTypes"
label="Scale Type" hide-details="auto"></v-select>
<v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact"
label="Select taxonomic rank" hide-details="auto" class="mx-2"></v-select>
</v-toolbar>
<v-card v-if="toValue(binPlotDataOptions) !== null" variant="flat">
<v-toolbar density="compact" color="transparent">
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="md:download" v-bind="props">
export
</v-btn>
</template>
<v-list>
<v-list-item value="svg">
<v-list-item-title
@click="downloadSvg(heatmapPlot, 'df-heatmap-systems-taxonomy.svg')">to
svg</v-list-item-title>
</v-list-item>
<v-list-item value="png">
<v-list-item-title
@click="downloadPng(heatmapPlot, 'df-heatmap-systems-taxonomy.png')">to
png</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<PlotFigure ref="heatmapPlot" :options="unref(binPlotDataOptions)" defer>
</PlotFigure>
</v-card>
</v-card>
<v-card v-else flat color="transparent">
<v-skeleton-loader type="card" :loading="pendingAllHits" height="400"></v-skeleton-loader>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<ServerDbTable title="RefSeq" :sortBy="sortBy"
:autocomplete-meili-facets-props="computedAutocompleteMeiliFacetsProps"
:data-table-server-props="dataTableServerProps" @refresh:search="(params) => getAllHits(params)">
<template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)">
</CollapsibleChips>
</template>
<template #[`item.type`]="{ item }">
<v-chip color="info" link size="small" :to="`/defense-systems/${item.type.toLowerCase()}`"
target="_blank"> {{
item.type }}
</v-chip>
</template>
<template #[`item.Assembly`]="{ item }">
<v-chip color="info" link size="small"
:href="`https://www.ncbi.nlm.nih.gov/datasets/genome/${item.Assembly}`" target="_blank"> {{
item.Assembly }}
</v-chip>
</template>
<template #[`item.replicon`]="{ item }">
<v-chip color="info" link size="small"
:href="`https://www.ncbi.nlm.nih.gov/nuccore/${item.replicon}`" target="_blank"> {{
item.replicon }}
</v-chip>
</template>
<!-- -->
<template #[`item.subtype`]="{ item }">
<v-chip color="info" link size="small" :to="`/defense-systems/${item.type.toLowerCase()}`"
target="_blank"> {{
item.subtype }}
</v-chip>
</template>
</ServerDbTable>
</v-card>
</v-card>
</template>