diff --git a/app/tools/activity/page.tsx b/app/tools/activity/page.tsx index 40cfc43..0105788 100644 --- a/app/tools/activity/page.tsx +++ b/app/tools/activity/page.tsx @@ -14,7 +14,7 @@ export default function Activity() { return (
- Mean : {round(mean(data), 2) } || Median : {round(median(data), 2)} || Mode : {round(mode(data), 2)} + Mean : {round(mean(data), 2) || ""} || Median : {round(median(data), 2) || ""} || Mode : {round(mode(data), 2) || ""}
) diff --git a/app/tools/ml/layout.tsx b/app/tools/ml/layout.tsx index 3349a45..d2fad66 100644 --- a/app/tools/ml/layout.tsx +++ b/app/tools/ml/layout.tsx @@ -59,15 +59,20 @@ export default function MLLayout({ children }) { {screenData.length > 0 && ( <> - setOneOffSmiles(e.target.value)} placeholder="Input Your SMILES string here">   - - setOneOffSmiles(smiles)} id="jsme_comp_1" /> - -   - -
- Predicted {target.activity_columns[0]}: {oneOffSMILESResult} +
+

Predict the activity of a single molecule

+ setOneOffSmiles(e.target.value)} placeholder="Input Your SMILES string here"> +
+ + setOneOffSmiles(smiles)} id="jsme_comp_1" /> + +
+ +
+ Predicted {target.activity_columns[0]}: {oneOffSMILESResult} +
+ Mean MAE: {round(mean(screenData[0]), 2)} || Mean R-Squared: {round(mean(screenData[1]), 2)} diff --git a/app/tools/toc/page.tsx b/app/tools/toc/page.tsx index 1c60326..a2480d8 100644 --- a/app/tools/toc/page.tsx +++ b/app/tools/toc/page.tsx @@ -1,17 +1,28 @@ "use client" -import { useContext } from "react"; +import { useContext, useState } from "react"; import Table from "../../../components/ui-comps/PaginatedTables"; import LigandContext from "../../../context/LigandContext"; import { usePapaParse } from 'react-papaparse'; +import TargetContext from "../../../context/TargetContext"; +import JSME from "../../../components/tools/toolViz/JSMEComp"; +import Dropdown from "../../../components/tools/toolViz/DropDown"; +import RDKitContext from "../../../context/RDKitContext"; +import { isEmpty } from "lodash"; -export default function TOC(){ +export default function TOC() { const { ligand } = useContext(LigandContext); + const { target } = useContext(TargetContext); const { jsonToCSV } = usePapaParse(); + const { rdkit } = useContext(RDKitContext); + + + const [searchSmi, setSearchSmi] = useState(''); + const [searchRes, setSearchRes] = useState(ligand); const results = jsonToCSV(ligand, { delimiter: ';' }); - function downloadCSV(csv: any){ + function downloadCSV(csv: any) { const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -22,11 +33,38 @@ export default function TOC(){ a.click(); document.body.removeChild(a); } - - return( + + function searchSubst() { + let searchResults = []; + let query = rdkit.get_mol(searchSmi); + ligand.map((lig) => { + let mol = rdkit.get_mol(lig.canonical_smiles); + let substructRes = mol.get_substruct_match(query); + var substructResJson = JSON.parse(substructRes); + if (!isEmpty(substructResJson)){ + searchResults.push(lig); + } + mol.delete(); + }) + query.delete(); + setSearchRes(searchResults); + } + + + return (
- - +
+ +
+ setSearchSmi(e.target.value)}/> +   + + setSearchSmi(smiles)} id="jsme_comp_2" /> + +   + +
+
) } \ No newline at end of file diff --git a/components/dataloader/DataLoader.tsx b/components/dataloader/DataLoader.tsx index 6830ea0..abe6afd 100644 --- a/components/dataloader/DataLoader.tsx +++ b/components/dataloader/DataLoader.tsx @@ -8,6 +8,7 @@ import { useRouter } from "next/navigation"; import ErrorContext from "../../context/ErrorContext"; import ModalComponent from "../ui-comps/ModalComponent"; import TabWrapper, { Tabs } from "../ui-comps/TabbedComponents"; +import SDFFileLoader from "./SDFFileLoader"; export default function DataLoader() { const { ligand, setLigand } = useContext(LigandContext); @@ -59,13 +60,20 @@ export default function DataLoader() {
- + - - + + + + + + + + + - +
diff --git a/components/dataloader/DataPreProcessToolKit.tsx b/components/dataloader/DataPreProcessToolKit.tsx index 9a535f2..b3a6a7b 100644 --- a/components/dataloader/DataPreProcessToolKit.tsx +++ b/components/dataloader/DataPreProcessToolKit.tsx @@ -19,7 +19,7 @@ type FingerPrintSettings = { const DataPreProcessToolKit = () => { const [loaded, setLoaded] = useState(true); const [stage, setStage] = useState('choose'); // Initial stage is 'choose' - const [selection, setSelection] = useState(null); + const [selection, setSelection] = useState('express'); const { ligand, setLigand } = useContext(LigandContext); const { rdkit } = useContext(RDKitContext); const { target, setTarget } = useContext(TargetContext); @@ -100,6 +100,7 @@ const DataPreProcessToolKit = () => { value="express" className='custom-radio' onChange={(e) => setSelection(e.target.value)} + defaultChecked = {true} /> Express diff --git a/components/dataloader/SDFFileLoader.tsx b/components/dataloader/SDFFileLoader.tsx new file mode 100644 index 0000000..0502e63 --- /dev/null +++ b/components/dataloader/SDFFileLoader.tsx @@ -0,0 +1,155 @@ +import { useContext, useState } from "react"; +import LigandContext from "../../context/LigandContext"; +import TargetContext from "../../context/TargetContext"; +import { useRouter } from "next/navigation"; +import ErrorContext from "../../context/ErrorContext"; +import sdfFileParser from "../utils/sdfFileParser"; +import { useForm } from "react-hook-form"; + + +export type Inputs = { + id_column: string; + act_column?: string; +}; + +export default function SDFFileLoader() { + const { ligand, setLigand } = useContext(LigandContext); + const { target, setTarget } = useContext(TargetContext); + const { setErrors } = useContext(ErrorContext); + const [stage, setStage] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + const router = useRouter(); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const handleFileChange = (event) => { + const input = event.target; + const file = input.files?.[0]; + if (file && file.name.endsWith(".sdf")) { + handleFile(file); + } else { + setErrors("Hey, please upload a valid SDF File"); + } + }; + + const handleFileDrop = (event) => { + event.preventDefault(); + const file = event.dataTransfer.files?.[0]; + if (file) { + handleFile(file); + } + }; + + const handleFile = (file) => { + console.log("File uploaded successfully:", file.name); + const reader = new FileReader(); + reader.onload = (e) => { + try { + const sdfContent = e.target?.result as string; + const molecules = sdfContent.split('$$$$').filter(molecule => molecule.trim() !== ''); + const parsedMolecules = molecules.map(molecule => sdfFileParser(molecule)); + setLigand(parsedMolecules); + setStage(true) + } catch (error) { + setErrors("Please upload a valid QITB JSON File"); + } + }; + reader.readAsText(file); + }; + + const handleDragOver = (event) => { + event.preventDefault(); + setIsHovered(true); + }; + + const handleDragExit = () => { + setIsHovered(false); + }; + + function fileSubmitHandler(data){ + let while_process_var = ligand.map((x) => { + x["id"] = x[data.id_column]; + x["canonical_smiles"] = x.molData; + x[data.act_column] = parseFloat(x[data.act_column]); + delete x[data.id_column], x[data.smi_column]; + return x; + }); + console.log(while_process_var) + setLigand(while_process_var); + setTarget({...target, data_source: "sdf", activity_columns: [data.act_column]}); + router.push("/tools/preprocess/"); + } + + if (stage) { + return ( +
+
+ + +
+ + + +
+ + + +
+ ) + } + + return ( +
+
{ + const fileInput = document.getElementById("fileInput"); + if (fileInput) { + fileInput.click(); + } + }} + className={`zone ${isHovered ? "zoneHover" : ""}`} + > +

Upload Your SDF File Here With Molecules. + You could also drag and drop the file here or Click to browse.

+ +
+
+ ); +} \ No newline at end of file diff --git a/components/dataloader/TargetGetter.tsx b/components/dataloader/TargetGetter.tsx index 62f00d5..3f0d302 100644 --- a/components/dataloader/TargetGetter.tsx +++ b/components/dataloader/TargetGetter.tsx @@ -67,7 +67,7 @@ export default function TargetGetter() { {loading ? ( ) : ( -
+
diff --git a/components/tools/toolViz/DropDown.tsx b/components/tools/toolViz/DropDown.tsx index ae84b63..444728c 100644 --- a/components/tools/toolViz/DropDown.tsx +++ b/components/tools/toolViz/DropDown.tsx @@ -15,49 +15,6 @@ const Dropdown = ({ buttonText, children }) => {
{children}
- ); }; diff --git a/components/ui-comps/PaginatedTables.tsx b/components/ui-comps/PaginatedTables.tsx index f59d357..6b13417 100644 --- a/components/ui-comps/PaginatedTables.tsx +++ b/components/ui-comps/PaginatedTables.tsx @@ -2,8 +2,9 @@ import React, { useState } from "react"; import TableFooter from "./TableFooter"; import useTable from "../../hooks/useTable"; import MoleculeStructure from "../tools/toolComp/MoleculeStructure"; +import { round } from "mathjs"; -const Table = ({ data, rowsPerPage }) => { +const Table = ({ data, rowsPerPage, act_column = [] }) => { const [page, setPage] = useState(1); const { slice, range } = useTable(data, page, rowsPerPage); @@ -15,16 +16,20 @@ const Table = ({ data, rowsPerPage }) => { - + {act_column.map((el, i) => ( + + ))} {slice.map((el, i) => ( - + - - + + {act_column.map((pl, j) => ( + + ))} ))} diff --git a/components/utils/fp_sorter.ts b/components/utils/fp_sorter.ts index 0585788..fe6eb21 100644 --- a/components/utils/fp_sorter.ts +++ b/components/utils/fp_sorter.ts @@ -1,7 +1,13 @@ import bitStringToBitVector from "./bit_vect"; export default function fpSorter(fpType : string, smilesString: string, rdkit, path? : number, nBits?: number){ - const mol = rdkit.get_mol(smilesString); + let mol; + try { + mol = rdkit.get_mol(smilesString); + } catch { + return null; + } + let molFP; if (fpType === "maccs"){ molFP = mol.get_maccs_fp(); @@ -13,5 +19,6 @@ export default function fpSorter(fpType : string, smilesString: string, rdkit, p throw new Error("Error has happened") } mol.delete(); - return bitStringToBitVector(molFP) + return bitStringToBitVector(molFP); + } \ No newline at end of file diff --git a/components/utils/sdfFileParser.ts b/components/utils/sdfFileParser.ts new file mode 100644 index 0000000..c128ed1 --- /dev/null +++ b/components/utils/sdfFileParser.ts @@ -0,0 +1,36 @@ +const sdfFileParser = (molecule) => { + const lines = molecule.split('\n'); + let molData = []; + let fields = {}; + let inMolData = true; + let fieldName = null; + + lines.forEach(line => { + line = line.trimStart(); // Trim whitespace characters from the beginning of the line + if (inMolData) { + if (line.startsWith('M END')) { + molData.push(line); + inMolData = false; + } else { + molData.push(line); + } + } else { + if (line.startsWith('> <')) { + const match = line.match(/> <(.*)>/); + if (match) { + fieldName = match[1]; + } + } else if (fieldName) { + fields[fieldName] = line.trim(); + fieldName = null; + } + } + }); + + return { + molData: molData.join('\n'), + ...fields + }; +}; + +export default sdfFileParser; \ No newline at end of file diff --git a/styles/form-comps.css b/styles/form-comps.css index a3806bd..b8bf04f 100644 --- a/styles/form-comps.css +++ b/styles/form-comps.css @@ -1,3 +1,11 @@ +.input { + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + margin-top: 8px; + min-width:200px; +} + /* Radio buttons with class 'custom-radio' */ input.custom-radio[type=radio] { --s: 1.5em; /* control the size */ @@ -49,3 +57,35 @@ input.custom-radio[type=radio]:disabled { margin: 5px 0; cursor: pointer; } + +.dropdown-container { + position: relative; + display: inline-block; +} + +.dropdown-icon { + background-color: var(--accent-color); + border: none; + color: white; + padding: 12px; + margin:5px; + font-size: 16px; + cursor: pointer; +} + +.dropdown-icon:hover { + background-color: #3e8e41; +} + +.dropdown-content { + display: none; + position: absolute; + z-index: 1; + top: 100%; /* Position it below the button */ + left: 50%; + transform: translateX(-50%); /* Center it horizontally */ +} + +.dropdown-content.visible { + display: block; +} \ No newline at end of file diff --git a/styles/index.css b/styles/index.css index f4cdb05..e0ea2e2 100644 --- a/styles/index.css +++ b/styles/index.css @@ -157,14 +157,6 @@ main { color: var(--text-color); } -.input { - padding: 8px; - border: 1px solid var(--border-color); - border-radius: 4px; - margin-top: 8px; - width: 80%; -} - .centered-self-container { height: 100%; gap: 10px; diff --git a/styles/tables.css b/styles/tables.css index d34bcff..dcbe43f 100644 --- a/styles/tables.css +++ b/styles/tables.css @@ -1,39 +1,4 @@ -.custom-table { - width: 100%; - border-collapse: collapse; - margin-bottom: 20px; - margin-top: 10px; - height: 100%; - overflow-y: auto; -} - -.custom-table th, -.custom-table td { - border: 1px solid var(--border-color); - padding: 10px; - text-align: left; -} - -.custom-table th { - background-color: var(--background-color); -} - -.custom-table tr:hover { - background-color: var(--secondary-color); -} - -@media screen and (max-width: 600px) { - .custom-table { - overflow-x: auto; - display: block; - font-size: x-small; - } - - .custom-table th, - .custom-table td { - white-space: nowrap; - } -} +/* Table Related Miscellaneous CSS */ .tableFooter { background-color: #f1f1f1; @@ -69,49 +34,91 @@ background: #f9f9f9; } -/* sdfadfasdfadsfaf */ +/* Table CSS */ .table { - border-collapse: collapse; - border: none; - width: 100%; - margin-top: 2%; + border: 1px solid #ccc; + border-collapse: collapse; + margin: 0; + padding: 0; + width: 100%; + table-layout: fixed; } -.tableRowHeader { - background-color: transparent; - transition: all 0.25s ease; - border-radius: 10px; +.table caption { + font-size: 1.5em; + margin: .5em 0 .75em; } -.tableHeader { - background-color: var(--tertiary-background-color); - padding: 12px; - font-weight: 500; - text-align: left; - font-size: 14px; - color: #2c3e50; +.table tr { + border: 1px solid #ddd; + padding: .35em; } -.tableHeader:first-child { - border-top-left-radius: 15px; +.table th, +.table td { + padding: .625em; + text-align: center; + word-wrap: break-word; + vertical-align: middle; } -.tableHeader:last-child { - border-top-right-radius: 15px; +.table th { + font-size: .85em; + letter-spacing: .1em; + text-transform: uppercase; + background-color: var(--tertiary-background-color); } -.tableRowItems { - cursor: auto; +.target-table tr:hover{ + background-color: var(--secondary-color); } -.tableRowItems:nth-child(odd) { - background-color: var(--secondary-color); -} +@media screen and (max-width: 1100px) { + .table { + border: 0; + } -.tableCell { - padding: 12px; - font-size: 14px; - color: var(--text-color); - vertical-align: middle; + .table caption { + font-size: 1.3em; + } + + .table thead { + border: none; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } + + .table tr { + border-bottom: 3px solid #ddd; + display: block; + margin-bottom: .625em; + } + + .table td { + border-bottom: 1px solid #ddd; + display: block; + font-size: .8em; + text-align: right; + } + + .table td::before { + /* + * aria-label has no advantage, it won't be read inside a table + content: attr(aria-label); + */ + content: attr(data-label); + float: left; + font-weight: bold; + text-transform: uppercase; + } + + .table td:last-child { + border-bottom: 0; + } }
Target Name ID SMILES Representation{slice.length > 0 && slice[0].predictions === null ? "Activity" : "Prediction"}{el}
{el.id} {el.canonical_smiles}{el.predictions} {el.neg_log_activity_column}{round(el[pl], 2)}