diff --git a/package-lock.json b/package-lock.json index 82006ef..600bd71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,14 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@tanstack/react-table": "^8.20.6", "electron-squirrel-startup": "^1.0.0", "framer-motion": "^11.0.25", "jotai": "^2.7.2", + "lucide-react": "^0.474.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@electron-forge/cli": "^7.3.1", @@ -3506,6 +3509,39 @@ "node": ">=10" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", + "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4050,6 +4086,15 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4787,6 +4832,19 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4931,6 +4989,15 @@ "node": ">=4" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5093,6 +5160,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -6855,6 +6934,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/framer-motion": { "version": "11.0.25", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.25.tgz", @@ -8489,6 +8577,15 @@ "node": ">=12" } }, + "node_modules/lucide-react": { + "version": "0.474.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", + "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -10783,6 +10880,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", @@ -11710,6 +11819,24 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11767,6 +11894,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -11984,6 +12132,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "packages/7zip-binaries": { + "version": "0.0.4", + "extraneous": true, + "license": "SEE LICENSE IN LICENSE" + }, + "packages/7zip-binaries.zip": { + "extraneous": true } } } diff --git a/package.json b/package.json index 62620c7..428c997 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hipstr-ui", "productName": "hipstr-ui", - "version": "1.0.0", + "version": "1.1.0", "description": "HipSTR software UI", "main": ".vite/build/main.js", "scripts": { @@ -44,11 +44,14 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@tanstack/react-table": "^8.20.6", "electron-squirrel-startup": "^1.0.0", "framer-motion": "^11.0.25", "jotai": "^2.7.2", + "lucide-react": "^0.474.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "xlsx": "^0.18.5" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.32.1" diff --git a/src/app.tsx b/src/app.tsx index 15bae56..9f86f0f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -4,6 +4,7 @@ import { BedTab } from "src/components/BedTab"; import { ExecutionTab } from "src/components/ExecutionTab"; import { FilesTab } from "src/components/FilesTab"; import { ParametersTab } from "src/components/ParametersTab"; +import { ResultsTab } from "src/components/ResultsTab"; import { hipstr } from "src/images"; import { theme } from "src/lib/theme"; @@ -28,6 +29,7 @@ export default function App() { BED Parameters Execution + Results @@ -41,7 +43,10 @@ export default function App() { setTabIndex(3)} /> - + setTabIndex(4)} /> + + + {}} /> diff --git a/src/components/ExecutionTab.tsx b/src/components/ExecutionTab.tsx index b7079c8..df54a8b 100644 --- a/src/components/ExecutionTab.tsx +++ b/src/components/ExecutionTab.tsx @@ -12,18 +12,18 @@ import { HStack, } from "@chakra-ui/react"; import { SaveDialogReturnValue } from "electron"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { FC, useEffect, useRef, useState } from "react"; import { SUPPORTED_PLATFORM_ARCHS } from "src/constants/global"; import { useGetPath } from "src/hooks/useGetPath"; import { usePathSeparator } from "src/hooks/usePathSeparator"; -import { bedAtom, fastaAtom, filesAtom, paramsAtom } from "src/jotai/execute"; +import { bedAtom, fastaAtom, filesAtom, paramsAtom, vcfPathAtom } from "src/jotai/execute"; import { osAtom } from "src/jotai/os"; import { joinPath } from "src/lib/path"; const spaces = " "; -export const ExecutionTab: FC = () => { +export const ExecutionTab: FC<{ onFinish: () => void }> = ({ onFinish }) => { const toast = useToast(); const [status, setStatus] = useState<"idle" | "executing" | "finished">("idle"); const [indexesOut, setIndexesOut] = useState(""); @@ -36,8 +36,13 @@ export const ExecutionTab: FC = () => { const params = useAtomValue(paramsAtom); const tempPath = useGetPath("temp"); const pathSep = usePathSeparator(); + const setVcfPath = useSetAtom(vcfPathAtom); + const strVcfPath = `${tempPath}/str_calls.vcf.gz`; useEffect(() => { + if (!strVcfPath) { + return; + } ipcRender.receive("main-to-render", (result: string | { exitCode: number }) => { if (typeof result === "string") { setCmdOut((prev) => `${prev}\n${result}`); @@ -51,12 +56,12 @@ export const ExecutionTab: FC = () => { status: "error", }); } + setVcfPath(strVcfPath); setStatus("finished"); } }); - }, []); + }, [strVcfPath]); - const strVcfPath = `${tempPath}/str_calls.vcf.gz`; const allParams: Record = { fasta, regions: bed, @@ -210,6 +215,9 @@ export const ExecutionTab: FC = () => { > Save VCF + ); diff --git a/src/components/ResultsTab.tsx b/src/components/ResultsTab.tsx new file mode 100644 index 0000000..dfd50ed --- /dev/null +++ b/src/components/ResultsTab.tsx @@ -0,0 +1,361 @@ +import { + VStack, + useToast, + Button, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + HStack, + Input, + Center, + Checkbox, + Select, + Text, +} from "@chakra-ui/react"; +import { FC, useMemo, useState } from "react"; +import { FileParameter } from "src/components/FileParameter"; +import { useAtom } from "jotai"; +import { bedAtom, vcfPathAtom } from "src/jotai/execute"; +import { getSamplesAndMarkersMap, SampleValues } from "src/lib/vcf"; +import { getMarkersMap, Marker, parseBed } from "src/lib/bed"; +import { useReactTable, getCoreRowModel, flexRender, getPaginationRowModel } from "@tanstack/react-table"; +import { utils as xlsxUtils, writeFile } from "xlsx"; + +export const ResultsTab: FC<{ onFinish: () => void }> = ({ onFinish }) => { + const [vcfPath, setVcfPath] = useAtom(vcfPathAtom); + const [bedPath] = useAtom(bedAtom); + const toast = useToast(); + const [markers, setMarkers] = useState([]); + const [markerSamplesMap, setMarkerSamplesMap] = useState<{ + [sampleId: string]: { [markerId: string]: SampleValues }; + }>({}); + + return ( + + { + if (!/\.vcf|\.vcf\.gz$/i.test(path)) { + toast({ + title: "File doesn't have .vcf.gz extension", + status: "error", + }); + return; + } + setVcfPath(path); + }} + /> + + + {markerSamplesMap && } + + ); +}; + +const ResultsTable: FC<{ + markers: Marker[]; + markerSamplesMap: { [markerId: string]: { [sampleId: string]: SampleValues } }; +}> = ({ markers: markersFromProps, markerSamplesMap }) => { + const [markerSearchTerm, setMarkerSearchTerm] = useState(""); + const [sampleSearchTerm, setSampleSearchTerm] = useState(""); + const [selectedColumns, setSelectedColumns] = useState([ + "gt", + "gb", + "q", + "pq", + // "ref", + // "period", + "allele1", + "allele2", + "dp", + ]); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const { markers, data } = useMemo(() => { + // Get unique markers + const markerSet = new Set(); + Object.entries(markerSamplesMap).forEach(([_, markers]) => { + Object.keys(markers).forEach((marker) => markerSet.add(marker)); + }); + const markers = Array.from(markerSet).filter((marker) => + marker.toLowerCase().includes(markerSearchTerm.toLowerCase()) + ); + + // Transform data to have samples as rows and markers as columns + const rows: any[] = []; + Object.entries(markerSamplesMap).forEach(([sample, markerValues]) => { + // Skip if sample doesn't match search term + if (!sample.toLowerCase().includes(sampleSearchTerm.toLowerCase())) { + return; + } + + const row: any = { sample }; + markers.forEach((marker) => { + const values = markerValues[marker]; + if (values) { + if (selectedColumns.includes("gt")) { + const [gt1, gt2] = values.gt?.split("|") || ["", ""]; + row[`${marker}_gt1`] = gt1; + row[`${marker}_gt2`] = gt2; + } + if (selectedColumns.includes("gb")) { + const [gb1, gb2] = values.gb?.split("|") || ["", ""]; + row[`${marker}_gb1`] = gb1; + row[`${marker}_gb2`] = gb2; + } + if (selectedColumns.includes("q")) row[`${marker}_q`] = values.q; + if (selectedColumns.includes("pq")) row[`${marker}_pq`] = values.pq; + const mk = Object.values(markersFromProps).find((m) => m.name === marker); + if (selectedColumns.includes("ref")) row[`${marker}_ref`] = mk?.refAllele; + if (selectedColumns.includes("period")) row[`${marker}_period`] = mk?.period; + if (selectedColumns.includes("allele1")) row[`${marker}_allele1`] = values.allele1; + if (selectedColumns.includes("allele2")) row[`${marker}_allele2`] = values.allele2; + if (selectedColumns.includes("dp")) row[`${marker}_dp`] = values.dp; + } + }); + rows.push(row); + }); + return { markers, data: rows }; + }, [markerSamplesMap, markerSearchTerm, sampleSearchTerm, selectedColumns]); + + const columns = useMemo(() => { + const cols: any[] = [ + { + header: "Sample", + accessorKey: "sample", + }, + ]; + + markers.forEach((marker) => { + const subColumns = [ + { key: "gt", header: "GT1", accessorKey: `${marker}_gt1` }, + { key: "gt", header: "GT2", accessorKey: `${marker}_gt2` }, + { key: "gb", header: "GB1", accessorKey: `${marker}_gb1` }, + { key: "gb", header: "GB2", accessorKey: `${marker}_gb2` }, + { key: "q", header: "Q", accessorKey: `${marker}_q` }, + { key: "pq", header: "PQ", accessorKey: `${marker}_pq` }, + { key: "ref", header: "REF", accessorKey: `${marker}_ref` }, + { key: "period", header: "Period", accessorKey: `${marker}_period` }, + { key: "allele1", header: "A1", accessorKey: `${marker}_allele1` }, + { key: "allele2", header: "A2", accessorKey: `${marker}_allele2` }, + { key: "dp", header: "DP", accessorKey: `${marker}_dp` }, + ].filter((col) => selectedColumns.includes(col.key)); + + cols.push({ + id: marker, + header: () =>
{marker}
, + columns: subColumns.map(({ header, accessorKey }) => ({ + header, + accessorKey, + })), + }); + }); + + return cols; + }, [markers, selectedColumns]); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + state: { + pagination: { + pageIndex, + pageSize, + }, + }, + onPaginationChange: (updater) => { + if (typeof updater === "function") { + const newState = updater({ pageIndex, pageSize }); + setPageIndex(newState.pageIndex); + setPageSize(newState.pageSize); + } else { + setPageIndex(updater.pageIndex); + setPageSize(updater.pageSize); + } + }, + }); + + const handleExportToExcel = async () => { + const worksheet = xlsxUtils.json_to_sheet([]); + // Create first row with marker names + const firstRow = [""]; + let colIndex = 1; // Start after sample column + markers.forEach((marker) => { + // Add marker name as first cell in the group + firstRow[colIndex] = marker; + // Fill remaining columns in group with empty strings + const extraCols = (selectedColumns.includes("gt") ? 1 : 0) + (selectedColumns.includes("gb") ? 1 : 0); + for (let i = 1; i < selectedColumns.length + extraCols; i++) { + firstRow[colIndex + i] = ""; + } + colIndex += selectedColumns.length + extraCols; + }); + xlsxUtils.sheet_add_json(worksheet, [firstRow], { skipHeader: true }); + xlsxUtils.sheet_add_json(worksheet, data, { origin: 1 }); + // Replace second row with column names + const secondRow = ["Sample"]; + markers.forEach(() => { + // Grab the first marker column and use the subcolumns + columns[1].columns.forEach((col: { header: string }) => { + secondRow.push(col.header); + }); + }); + xlsxUtils.sheet_add_json(worksheet, [secondRow], { skipHeader: true, origin: "A2" }); + + const workbook = xlsxUtils.book_new(); + xlsxUtils.book_append_sheet(workbook, worksheet, "Results"); + writeFile(workbook, "results.xlsx"); + }; + + return ( + + + + setSampleSearchTerm(e.target.value)} + size="sm" + /> + setMarkerSearchTerm(e.target.value)} + size="sm" + /> + + + + {[ + { key: "gt", label: "GT" }, + { key: "gb", label: "GB" }, + { key: "q", label: "Q" }, + { key: "pq", label: "PQ" }, + { key: "ref", label: "Ref Allele" }, + { key: "period", label: "Period" }, + { key: "allele1", label: "Allele 1" }, + { key: "allele2", label: "Allele 2" }, + { key: "dp", label: "DP" }, + ].map(({ key, label }) => ( + { + if (e.target.checked) { + setSelectedColumns([...selectedColumns, key]); + } else { + setSelectedColumns(selectedColumns.filter((col) => col !== key)); + } + }} + > + {label} + + ))} + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ + + + + + + Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} + + + +
+ ); +}; diff --git a/src/jotai/execute.ts b/src/jotai/execute.ts index 0d30f25..739ab12 100644 --- a/src/jotai/execute.ts +++ b/src/jotai/execute.ts @@ -7,3 +7,4 @@ export const filesAtom = atomWithLocalStorage("files", []); export const fastaAtom = atomWithLocalStorage("fasta", ""); export const bedAtom = atomWithLocalStorage("bed", ""); export const paramsAtom = atomWithLocalStorage("params", {}); +export const vcfPathAtom = atomWithLocalStorage("vcfPath", ""); diff --git a/src/lib/bed.ts b/src/lib/bed.ts new file mode 100644 index 0000000..9ddc15f --- /dev/null +++ b/src/lib/bed.ts @@ -0,0 +1,32 @@ +export type Marker = { + chrom: string; + start: number; + end: number; + period: number; + refAllele: number; + name: string; +}; + +export function parseBed(bedContent: string): Marker[] { + const lines = bedContent.split("\n"); + const markers = lines.map((line) => { + const [chrom, start, end, period, refAllele, name] = line.split("\t"); + return { + chrom, + start: parseInt(start, 10), + end: parseInt(end, 10), + period: parseInt(period, 10), + refAllele: parseInt(refAllele, 10), + name, + }; + }); + return markers; +} + +export function getMarkersMap(markers: Marker[]) { + const markersMap: { [markerId: string]: Marker } = {}; + markers.forEach((marker) => { + markersMap[marker.name] = marker; + }); + return markersMap; +} diff --git a/src/lib/vcf.ts b/src/lib/vcf.ts new file mode 100644 index 0000000..acbf919 --- /dev/null +++ b/src/lib/vcf.ts @@ -0,0 +1,60 @@ +import { Marker } from "src/lib/bed"; + +export type SampleValues = { + gt: string; + gb: string; + q: string; + pq: string; + dp: string; + allele1: number; + allele2: number; +}; + +export function getSamplesAndMarkersMap(vcfContent: string, markersMap: { [markerId: string]: Marker }) { + const markerSamplesMap: { + [sampleId: string]: { + [markerId: string]: SampleValues; + }; + } = {}; + const lines = vcfContent.split("\n"); + let samples: string[] = []; + for (const line of lines) { + if (line.startsWith("#CHROM")) { + samples = line.split("\t").slice(9); + samples.forEach((sample) => { + markerSamplesMap[sample] = {}; + }); + } + } + for (const line of lines) { + if (line.startsWith("#")) { + continue; + } + const [chrom, pos, id, ref, alt, qual, filter, info, format, ...values] = line.split("\t"); + values.forEach((sampleValues, index) => { + if (!sampleValues) { + return; + } + if (!format.startsWith("GT:GB:Q:PQ:DP")) { + throw new Error("Invalid format, expecting GT:GB:Q:PQ:DP"); + } + const [gt, gb, q, pq, dp] = sampleValues.split(":"); + const [bp1, bp2] = gb ? gb.split("|") : []; + const allele1 = gb ? markersMap[id].refAllele + parseInt(bp1, 10) / markersMap[id].period : null; + const allele2 = gb ? markersMap[id].refAllele + parseInt(bp2, 10) / markersMap[id].period : null; + markerSamplesMap[samples[index]][id] = { + gt, + gb, + q, + pq, + dp, + allele1, + allele2, + }; + }); + } + return { + samples, + markerSamplesMap, + }; +} diff --git a/src/main.ts b/src/main.ts index 6f90fc7..dddf1e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -86,29 +86,32 @@ ipcMain.handle("getFilesFromFolder", async (event: IpcMainInvokeEvent, path: str ); }); -ipcMain.handle("execute", async (event: IpcMainInvokeEvent, {command, logToFile}: {command: string, logToFile: boolean}) => { - let handle: number | undefined; - if (logToFile) { - const tempPath = app.getPath("temp"); - handle = fs.openSync(`${tempPath}/log.txt`, "w"); +ipcMain.handle( + "execute", + async (event: IpcMainInvokeEvent, { command, logToFile }: { command: string; logToFile: boolean }) => { + let handle: number | undefined; + if (logToFile) { + const tempPath = app.getPath("temp"); + handle = fs.openSync(`${tempPath}/log.txt`, "w"); + } + const proc = child_process.spawn(command, [], { + shell: true, + stdio: ["ignore", "pipe", "pipe"], + }); + proc.stdout.on("data", (data) => { + handle && fs.appendFileSync(handle, data.toString()); + mainWindow.webContents.send("main-to-render", data.toString()); + }); + proc.stderr.on("data", (data) => { + handle && fs.appendFileSync(handle, data.toString()); + mainWindow.webContents.send("main-to-render", data.toString()); + }); + proc.on("close", (code) => { + handle && fs.closeSync(handle); + mainWindow.webContents.send("main-to-render", { exitCode: code }); + }); } - const proc = child_process.spawn(command, [], { - shell: true, - stdio: ["ignore", "pipe", "pipe"], - }); - proc.stdout.on("data", (data) => { - handle && fs.appendFileSync(handle, data.toString()); - mainWindow.webContents.send("main-to-render", data.toString()); - }); - proc.stderr.on("data", (data) => { - handle && fs.appendFileSync(handle, data.toString()); - mainWindow.webContents.send("main-to-render", data.toString()); - }); - proc.on("close", (code) => { - handle && fs.closeSync(handle); - mainWindow.webContents.send("main-to-render", { exitCode: code }); - }); -}); +); ipcMain.handle("execSync", (event: IpcMainInvokeEvent, command: string) => { return child_process.execSync(command).toString(); @@ -157,3 +160,13 @@ ipcMain.handle("render-to-main-to-render", (event, message) => { ipcMain.handle("getPath", (event: IpcMainInvokeEvent, name: GetPathName) => { return app.getPath(name); }); + +ipcMain.handle("extractGz", async (event: IpcMainInvokeEvent, path: string) => { + try { + await child_process.execSync(`gunzip -fdk ${path}`).toString(); + return true; + } catch (error) { + console.error(error); + return error; + } +}); diff --git a/src/preload.ts b/src/preload.ts index 4626173..8bf95de 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -15,6 +15,7 @@ const electronHandler = { dirName: () => ipcRenderer.invoke("dirName"), execSync: (command: string) => ipcRenderer.invoke("execSync", command), getPath: (name: GetPathName) => ipcRenderer.invoke("getPath", name), + extractGz: (path: string) => ipcRenderer.invoke("extractGz", path), }; contextBridge.exposeInMainWorld("electron", electronHandler);