diff --git a/components/content/RefseqDb.vue b/components/content/RefseqDb.vue index dc286eec6c145bcd51f0707393f3a2f721e4acce..e5ef7c958dbc2e701135cb5ec788f50f770d44c7 100644 --- a/components/content/RefseqDb.vue +++ b/components/content/RefseqDb.vue @@ -4,14 +4,22 @@ import PlotFigure from "~/components/PlotFigure"; import { useDisplay } from "vuetify"; import type { SortItem } from "@/components/ServerDbTable.vue" import { ServerDbTable } from "#components" +import { useSerialize } from "@/composables/useSerialize"; +import { useRasterize } from "@/composables/useRasterize"; +import { useDownloadBlob } from '@/composables/useDownloadBlob'; +import type { ComponentPublicInstance } from 'vue' + const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }]) const itemValue = ref("id"); const { width } = useDisplay(); const scaleTransform: Ref<string[]> = ref([]) +const { serialize } = useSerialize() +const { rasterize } = useRasterize() +const { download } = useDownloadBlob() const facets = ref([ "replicon", @@ -32,6 +40,8 @@ const availableTaxo: Ref<string[]> = ref([ "phylum", "Superkingdom" ]); + +const scaleTypes = ref<string[]>(['linear', 'sqrt', 'log', 'symlog']) const selectedTaxoRank = ref("phylum"); const headers = ref([ @@ -121,22 +131,25 @@ const dataTableServerProps = computed(() => { const defaultBarPlotOptions = computed(() => { return { - x: { label: null, tickRotate: 45, ticks: 10 }, - y: { grid: true, type: scaleType.value }, - color: { legend: true }, - width: computedWidth.value, + 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() + return Object.entries(toValue(msResult).facetDistribution.type) + .map(([key, value]) => { + return { + type: key, + count: value + } + }).sort() } else { return [] } }) @@ -144,13 +157,15 @@ const computedDistriSystemOptions = computed(() => { return { ...defaultBarPlotOptions.value, marginBottom: 100, + y: { ...defaultBarPlotOptions.value.y, type: toValue(scaleType), label: "Count" }, + x: { ...defaultBarPlotOptions.value.x, label: "Systems" }, + width: computedWidth.value, marks: [ // Plot.frame(), Plot.barY( toValue(computedSystemDistribution), { y: "count", x: 'type', tip: true, - // fill: "#6750a4", sort: { x: "-y" }, }, @@ -158,6 +173,9 @@ const computedDistriSystemOptions = computed(() => { ], }; }); + + +// Taxo distri const computedTaxonomyDistribution = computed(() => { if (toValue(msResult)?.facetDistribution?.[selectedTaxoRank.value]) { return Object.entries(toValue(msResult).facetDistribution[selectedTaxoRank.value]).map(([key, value]) => { @@ -174,6 +192,9 @@ const computedDistriTaxoOptions = computed(() => { return { ...defaultBarPlotOptions.value, 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), @@ -212,8 +233,8 @@ const binPlotOptions = ref({ marginBottom: 200, padding: 0, grid: true, - x: { tickRotate: 90, tip: true, }, - + x: { tickRotate: 90, tip: true, label: "Systems" }, + // y: { tickFormat: 's' }, color: { scheme: "turbo", legend: true }, }) @@ -225,7 +246,11 @@ const binPlotDataOptions = computed(() => { color: { ...binPlotOptions.value.color, - type: scaleType.value + type: scaleType.value, + tickFormat: '~s', + ticks: scaleType.value === 'symlog' ? 3 : 5, + // width: 350 + }, // fy: { domain: groupSortDomain.value }, marks: [ @@ -236,6 +261,23 @@ const binPlotDataOptions = computed(() => { }) 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> @@ -253,21 +295,72 @@ const scaleType = ref("linear") <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="['linear', 'sqrt', 'symlog']" label="Scale Type" hide-details="auto"></v-select> + <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"> - <PlotFigure :options="unref(computedDistriSystemOptions)" defer></PlotFigure> - + <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"> - - <PlotFigure defer :options="unref(computedDistriTaxoOptions)"></PlotFigure> - + <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> @@ -285,19 +378,42 @@ const scaleType = ref("linear") <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="['linear', 'sqrt', 'symlog']" label="Scale Type" hide-details="auto"></v-select> + <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> - <PlotFigure v-if="toValue(binPlotDataOptions) !== null" :options="unref(binPlotDataOptions)" - defer> - </PlotFigure> + <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-skeleton-loader type="card" :loading="pendingAllHits" height="400"></v-skeleton-loader> </v-card> </v-expansion-panel-text> </v-expansion-panel> diff --git a/composables/useCsvDownload.ts b/composables/useCsvDownload.ts index a84e6ad90db80aaf3a27d3f9c0acef621fc25b26..ffbf6e7b517664010b224b8202b135286a9d072c 100644 --- a/composables/useCsvDownload.ts +++ b/composables/useCsvDownload.ts @@ -1,5 +1,7 @@ import Papa from 'papaparse'; // import { saveAs } from "file-saver"; +import { useDownloadBlob } from './useDownloadBlob'; +const { download } = useDownloadBlob() export function useCsvDownload( rawData: MaybeRef<Record<string, any>>, @@ -29,12 +31,7 @@ export function useCsvDownload( }) const csvContent = Papa.unparse(toValue(data), { columns: toValue(columns) }); blob.value = new Blob([csvContent], { type: "text/csv" }); - var a = document.createElement("a"); - a.href = URL.createObjectURL(blob.value); - a.download = filename.value; - a.click(); - URL.revokeObjectURL(a.href); - + download(blob, filename) } return { data, filename } } diff --git a/composables/useDownloadBlob.ts b/composables/useDownloadBlob.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ae7b07beab9547b5e2e9d6ae992271f03a01a1e --- /dev/null +++ b/composables/useDownloadBlob.ts @@ -0,0 +1,14 @@ +export function useDownloadBlob() { + function download(blob: MaybeRef<Blob>, filename: MaybeRef<string>) { + const toValueBlob = toValue(blob) + const toValueFilename = toValue(filename) + var a = document.createElement("a"); + a.href = URL.createObjectURL(toValueBlob); + a.download = toValueFilename; + a.click(); + URL.revokeObjectURL(a.href); + } + + return { download } + +} \ No newline at end of file diff --git a/composables/useRasterize.ts b/composables/useRasterize.ts new file mode 100644 index 0000000000000000000000000000000000000000..27b513b0087a972e9d2ada62b4e137858b33f6be --- /dev/null +++ b/composables/useRasterize.ts @@ -0,0 +1,47 @@ +import { useDownloadBlob } from './useDownloadBlob'; +import { useSerialize } from './useSerialize'; +import { useSvgPlot } from './useSvgPlot'; +const { serialize } = useSerialize() + +export function useRasterize() { + + function rasterize(component: MaybeRef<ComponentPublicInstance | null>, filename: MaybeRef<string>) { + const toValueCompo = toValue(component) + + if (toValueCompo !== null) { + const { svg } = useSvgPlot(toValueCompo) + const toValueSvg = toValue(svg) + if (toValueSvg !== null) { + let resolve, reject; + const promise: Promise<Blob> = new Promise((y, n) => (resolve = y, reject = n)); + const image = new Image; + image.onerror = reject; + + console.log(toValueSvg) + image.onload = () => { + console.log("try to get boundingclientRect") + const rect = toValueSvg.getBoundingClientRect(); + console.log(rect) + const canvas = document.createElement("canvas"); + canvas.width = rect.width + canvas.height = rect.height + const ctx = canvas.getContext("2d") + + + if (ctx !== null) { + ctx.drawImage(image, 0, 0, rect.width, rect.height); + ctx.canvas.toBlob(resolve); + } + } + const blob = toValue(serialize(component)) + if (blob !== undefined) { + image.src = URL.createObjectURL(blob); + } + return promise; + } + } + + } + return { rasterize } + +} \ No newline at end of file diff --git a/composables/useSerialize.ts b/composables/useSerialize.ts new file mode 100644 index 0000000000000000000000000000000000000000..47bcbc451f39d721fb4e6527bb2f77303c66bbad --- /dev/null +++ b/composables/useSerialize.ts @@ -0,0 +1,37 @@ +import { useSvgPlot } from './useSvgPlot'; + + +export function useSerialize() { + const xmlns = ref("http://www.w3.org/2000/xmlns/"); + const xlinkns = ref("http://www.w3.org/1999/xlink"); + const svgns = ref("http://www.w3.org/2000/svg"); + const blob = ref<Blob>() + + function serialize(compo: MaybeRef<ComponentPublicInstance | null>) { + const toValueCompo = toValue(compo) + if (toValueCompo !== null) { + const { svg } = useSvgPlot(toValueCompo) + const toValueSvg = toValue(svg) + if (toValueSvg !== null) { + const clonedSvg = toValueSvg.cloneNode(true); + const fragment = window.location.href + "#"; + const walker = document.createTreeWalker(toValueSvg, NodeFilter.SHOW_ELEMENT); + while (walker.nextNode()) { + for (const attr of walker.currentNode.attributes) { + if (attr.value.includes(fragment)) { + attr.value = attr.value.replace(fragment, "#"); + } + } + } + clonedSvg.setAttributeNS(xmlns.value, "xmlns", svgns.value); + clonedSvg.setAttributeNS(xmlns.value, "xmlns:xlink", xlinkns.value); + const serializer = new window.XMLSerializer; + const string = serializer.serializeToString(clonedSvg); + blob.value = new Blob([string], { type: "image/svg+xml" }); + return blob + } + else { return undefined } + } + } + return { serialize } +} \ No newline at end of file diff --git a/composables/useSvgPlot.ts b/composables/useSvgPlot.ts new file mode 100644 index 0000000000000000000000000000000000000000..7409a988d73990b4c96fe7cf86baf81883db9f63 --- /dev/null +++ b/composables/useSvgPlot.ts @@ -0,0 +1,8 @@ +export function useSvgPlot(component: MaybeRef<ComponentPublicInstance>) { + const svg = ref<SVGElement | null>(null) + const toValueCompo = toValue(component) + const rootElem = toValueCompo.$el + svg.value = rootElem.querySelector("svg.plot") + + return { svg } +} \ No newline at end of file