diff --git a/composables/useFetchMsDocument.ts b/composables/useFetchMsDocument.ts index 5a3385c922b549f1afe3eefe0550c9124d0c1b96..814b2976467845818440be14cfdcbdc85017968a 100644 --- a/composables/useFetchMsDocument.ts +++ b/composables/useFetchMsDocument.ts @@ -1,7 +1,15 @@ import { MeiliSearch } from 'meilisearch' import { useRuntimeConfig, watchEffect, type MaybeRef, ref, toValue } from '#imports' - -export function useFetchMsDocument(index: MaybeRef<string> = "", search: MaybeRef<string> = "", filter: MaybeRef<string> = null, limit: MaybeRef<number> = 1000) { +import type { FacetDistribution, Hits } from 'meilisearch'; +export function useFetchMsDocument( + index: MaybeRef<string> = ref(""), + search: Ref<string> = ref(""), + filter: Ref<string> = ref(''), + limit: Ref<number> = ref(1000), + hitsPerPage: Ref<number> = ref(25), + page: Ref<number> = ref(1), + facets: Ref<string[]> = ref([]) +) { const runtimeConfig = useRuntimeConfig(); const client = new MeiliSearch({ @@ -10,19 +18,35 @@ export function useFetchMsDocument(index: MaybeRef<string> = "", search: MaybeRe }) const pending = ref(false) const filterError = ref(null) - const hits = ref([]) + const hits: Ref<Hits<Record<string, any>>> = ref([]) const totalHits = ref(0) + const totalPages = ref(0) + const facetDistribution: Ref<FacetDistribution | undefined> = ref({}) + // reset page when filter and search change + watch(filter, () => { + page.value = 1 + + }) + watch(search, () => { + page.value = 1 + + }) watchEffect(async () => { try { pending.value = true const res = await client.index(toValue(index)).search(toValue(search), { limit: toValue(limit), - filter: toValue(filter) + filter: toValue(filter), + hitsPerPage: toValue(hitsPerPage), + page: toValue(page), + facets: toValue(facets) }) filterError.value = null - const { hits: resHits, estimatedTotalHits } = res - totalHits.value = estimatedTotalHits + const { hits: resHits, totalHits: resTotalHits, totalPages: resTotalPages, facetDistribution: facetD } = res + totalHits.value = resTotalHits hits.value = resHits + totalPages.value = resTotalPages + facetDistribution.value = facetD } catch ({ code, message }) { if (code === 'invalid_search_filter') { filterError.value = message @@ -32,6 +56,6 @@ export function useFetchMsDocument(index: MaybeRef<string> = "", search: MaybeRe } }) - return { hits, totalHits, pending, filterError } + return { hits, totalHits, pending, filterError, totalPages, facetDistribution } } diff --git a/pages/refseq.vue b/pages/refseq.vue index be42cde2212f5c599f71c245dbac8d252193e39b..692eb59da44eb931e4295b3905bedf6bf7a68e81 100644 --- a/pages/refseq.vue +++ b/pages/refseq.vue @@ -3,9 +3,10 @@ import * as Plot from "@observablehq/plot"; import PlotFigure from "~/components/PlotFigure"; import { useDisplay } from "vuetify"; import JsonCSV from 'vue-json-csv'; +import { useFacetsStore, type Facets } from '~~/stores/facets' const runtimeConfig = useRuntimeConfig(); - +const facetStore = useFacetsStore() const { width, height } = useDisplay(); const minTableHeight = ref(400) const computedTableHeight = computed(() => { @@ -30,10 +31,12 @@ const plotHeight = computed(() => { }); // const filterError = ref(null) const search: Ref<string> = ref(""); -const filter = ref(null) -const limit = ref(500000) +const filter: Ref<string> = ref('') +const hitsPerPage: Ref<number> = ref(25) +const limit = ref(1000) const itemValue = ref("id"); - +const facets = ref(["type"]) +const page = ref(1) const prependHeaders = ref([ { title: "Replicon", key: "replicon" }, { @@ -54,11 +57,40 @@ const appendHeaders = ref([ } ]) + + function capitalize([first, ...rest]) { return first.toUpperCase() + rest.join('').toLowerCase(); } +const computedFacets = computed(() => { + return [...facets.value, selectedTaxoRank.value] +}) + + +const { hits: items, pending, totalHits: itemsLength, filterError, facetDistribution } = useFetchMsDocument("refseq", search, filter, limit, hitsPerPage, page, computedFacets) + +watch(facetDistribution, (facetDistri) => { + + facetStore.setFacets({ facetDistribution: facetDistri, facetStat: undefined }) +}) +const computedSystemDistribution = computed(() => { + if (facetDistribution.value?.type) { + return Object.entries(facetDistribution.value.type).map(([key, value]) => { + return { type: key, count: value } + }).sort() + } else { return [] } + +}) + +const computedTaxonomyDistribution = computed(() => { + if (facetDistribution.value?.[selectedTaxoRank.value]) { + return Object.entries(facetDistribution.value[selectedTaxoRank.value]).map(([key, value]) => { + return { [selectedTaxoRank.value]: key, count: value } + }).sort() + } else { return [] } + +}) -const { hits: refseqData, pending, totalHits, filterError } = useFetchMsDocument("refseq", search, filter, limit) const computedHeaders = computed(() => { return [...prependHeaders.value, ...availableTaxo.value.map(taxo => { @@ -71,100 +103,84 @@ const computedHeaders = computed(() => { function itemToFilter(item, key) { - const value = item[key] - const filterToAdd = /\s/g.test(value) ? `${key}="${value}"` : `${key}=${value}` - return filter.value === null ? filterToAdd : `${filter.value} AND ${filterToAdd}` + return filter.value === '' ? filterToAdd : `${filter.value} AND ${filterToAdd}` } const itemFilterKeys = computed(() => { return [...availableTaxo.value, 'type', 'subtype'] }) - -const computedDistriSystemOptions = computed(() => { - const groupYOption = facetDistriSystem.value - ? { - fx: "type", - x: selectedTaxoRank.value, - // fill: selectedTaxoRank.value, - tip: true, - sort: { y: "-x" }, - } - : { - x: "type", - tip: true, - fill: "#6750a4", - // fill: selectedTaxoRank.value, - sort: { x: "-y" }, - }; - +const defaultBarPlotOptions = computed(() => { return { - marginLeft: 30, - marginBottom: 120, x: { label: null, tickRotate: 70 }, - y: { grid: true }, + y: { nice: true, grid: true }, color: { legend: true }, width: computedWidth.value, height: plotHeight.value, + } +}) +const computedDistriSystemOptions = computed(() => { + return { + ...defaultBarPlotOptions.value, + marginBottom: 120, marks: [ // Plot.frame(), Plot.barY( - unref(refseqData.value), - Plot.groupX( - { y: "count" }, - groupYOption - ) + toValue(computedSystemDistribution), + { + y: "count", x: 'type', tip: true, + fill: "#6750a4", + sort: { x: "-y" }, + }, + ), ], }; }); const computedDistriTaxoOptions = computed(() => { - const groupYOption = { - x: selectedTaxoRank.value, - // fx: selectedTaxoRank.value, - tip: true, - // fill: "type", - fill: "#6750a4", - // offset: "normalize", - sort: { x: "-y" }, - }; - return { - // marginLeft: 110, - marginBottom: 100, - grid: true, - x: { label: null, tickRotate: 70 }, - // x: { label: facet.value ? "Count" : null, tickRotate: 90 }, - // y: { nice: true }, - color: { legend: true }, - width: computedWidth.value, - height: plotHeight.value, + ...defaultBarPlotOptions.value, + marginBottom: 200, marks: [ - // Plot.frame(), Plot.barY( - unref(refseqData.value), - Plot.groupX({ y: "count" }, groupYOption) + toValue(computedTaxonomyDistribution), + { + y: "count", + x: selectedTaxoRank.value, + tip: true, + fill: "#6750a4", + sort: { x: "-y" }, + } ), ], }; }); -const datatable = ref(null) - +// const datatable = ref(null) +const hasToGenerateDownload = ref(false) +let itemsToDownload = ref() + +watch(hasToGenerateDownload, (val) => { + console.log(val) + if (val === true) { + const { hits: items, pending, totalHits: itemsLength, filterError, facetDistribution } = useFetchMsDocument("refseq", search, filter, limit, hitsPerPage, page, computedFacets) + itemsToDownload.value = items.value + } +}) </script> <template> <v-card flat> <v-toolbar color="primary" density="compact"> <v-app-bar-nav-icon></v-app-bar-nav-icon> - <v-toolbar-title>RefSeq Entries ({{ totalHits }}) + <v-toolbar-title>RefSeq Entries ({{ itemsLength }}) </v-toolbar-title> - <JsonCSV :data="refseqData" name="refseq-defense-system.csv"> - <v-btn icon> + <JsonCSV :data="itemsToDownload" name="refseq-defenes-system.csv"> + <v-btn icon @click="hasToGenerateDownload = true"> <v-icon icon="md:download"></v-icon> - <v-tooltip activator="parent" location="bottom">Download {{ refseqData.length }} entries</v-tooltip> + <v-tooltip activator="parent" location="bottom">Download {{ itemsLength }} entries</v-tooltip> </v-btn> </JsonCSV> </v-toolbar> @@ -181,32 +197,30 @@ const datatable = ref(null) <v-col cols="auto"> <v-text-field v-model="filter" prepend-inner-icon="mdi-magnify" label="Filter" hide-details="auto" class="mx-2" clearable :error-messages="filterError"></v-text-field></v-col> - <v-data-table-virtual ref="datatable" fixed-header :loading="pending" :headers="computedHeaders" :items="refseqData" - :item-value="itemValue" density="compact" :height="computedTableHeight" class="elevation-1 mt-2"> + <v-data-table-server v-model:page="page" v-model:items-per-page="hitsPerPage" fixed-header :loading="pending" + :headers="computedHeaders" :items="items" :items-length="itemsLength" :item-value="itemValue" density="compact" + :height="computedTableHeight" class="elevation-1 mt-2"> <template #[`item.${key}`]="{ item }" v-for="key in itemFilterKeys" :key="key"> <v-chip @click="filter = itemToFilter(item, key)">{{ item[key] }}</v-chip> </template> <template #[`item.accession_in_sys`]="{ item }"> <accession-chips :accessions="item.accession_in_sys" baseUrl="http://toto.pasteur.cloud"></accession-chips> </template> - </v-data-table-virtual> + </v-data-table-server> </v-card> <v-card flat class="my-3" :loading="pending"> <v-card-title> Systems Distribution</v-card-title> - <!-- <v-toolbar density="compact"><v-toolbar-title> Distribution Systems</v-toolbar-title></v-toolbar> --> <v-card-text> <PlotFigure :options="unref(computedDistriSystemOptions)" defer></PlotFigure> </v-card-text> </v-card> <v-card flat :loading="pending"> <v-card-title> Taxonomic Distribution</v-card-title> - <!-- <v-toolbar density="compact"><v-toolbar-title> Distribution Taxonomy</v-toolbar-title></v-toolbar> --> <v-card-text> <v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact" label="Select taxonomic rank"></v-select> - <!-- <v-switch v-model="facet" label="Facet" color="primary"></v-switch> --> <PlotFigure defer :options="unref(computedDistriTaxoOptions)"></PlotFigure> </v-card-text> </v-card> diff --git a/stores/facets.ts b/stores/facets.ts new file mode 100644 index 0000000000000000000000000000000000000000..4efeb6bfb1f557b25d13f2ded3582bc4f76f3c3c --- /dev/null +++ b/stores/facets.ts @@ -0,0 +1,23 @@ +import { defineStore } from 'pinia' +import type { FacetDistribution, FacetStat } from 'meilisearch'; + + +export interface Facets { + facetDistribution: FacetDistribution | undefined; + facetStat: FacetStat | undefined; +} + +export const useFacetsStore = defineStore('facets', () => { + + + const facets: Ref<Facets> = ref({ facetDistribution: undefined, facetStat: undefined }) + + + function setFacets(newFacets: Facets) { + facets.value = newFacets + + } + + + return { facets, setFacets } +}) \ No newline at end of file