From 5596eafe4f95bbf5eee3fb661fb21d05fa23def1 Mon Sep 17 00:00:00 2001 From: Jonathan Fisher <49597360+jpfisher72@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:03:53 -0400 Subject: [PATCH] Small improvements (#70) * Footer height, refactor filters * Fix to prevent error on empty table * Simplify filters code a bit --- screen2.0/src/app/globals.css | 4 +- screen2.0/src/common/components/Footer.tsx | 1 - .../common/components/MainResultsFilters.tsx | 394 +++++++----------- .../common/components/MainResultsTable.tsx | 11 +- screen2.0/src/common/lib/filter-helpers.ts | 170 +++++++- screen2.0/yarn.lock | 4 +- 6 files changed, 321 insertions(+), 263 deletions(-) diff --git a/screen2.0/src/app/globals.css b/screen2.0/src/app/globals.css index c667a949..b0e275f3 100644 --- a/screen2.0/src/app/globals.css +++ b/screen2.0/src/app/globals.css @@ -390,9 +390,9 @@ template { min-height: 100vh; } -/* Leaves exactly enough space on the bottom of the page for the footer */ +/* Leaves exactly enough space on the bottom of the page for the footer + 2rem */ #content-wrapper { - padding-bottom: 4rem; + padding-bottom: 6rem; } /* Limit the amount of space the content can take up so it doesn't run into edge */ diff --git a/screen2.0/src/common/components/Footer.tsx b/screen2.0/src/common/components/Footer.tsx index a9f32649..2109f990 100644 --- a/screen2.0/src/common/components/Footer.tsx +++ b/screen2.0/src/common/components/Footer.tsx @@ -5,7 +5,6 @@ import MuiLink from "@mui/material/Link" export default function Footer() { return ( - //This positioning needs to change. Need it to be attached to bottom by scroll. Not attached by attaching to bottom of viewport {"Copyright © "} diff --git a/screen2.0/src/common/components/MainResultsFilters.tsx b/screen2.0/src/common/components/MainResultsFilters.tsx index 28f64b44..d06cc30e 100644 --- a/screen2.0/src/common/components/MainResultsFilters.tsx +++ b/screen2.0/src/common/components/MainResultsFilters.tsx @@ -25,10 +25,10 @@ import Grid2 from "@mui/material/Unstable_Grid2" import Link from "next/link" import { RangeSlider, DataTable } from "@weng-lab/psychscreen-ui-components" -import { useState, useMemo } from "react" +import { useState, useMemo, useCallback } from "react" import { useRouter } from "next/navigation" import { CellTypeData, FilteredBiosampleData, MainQueryParams, UnfilteredBiosampleData } from "../../app/search/types" -import { outputT_or_F } from "../lib/filter-helpers" +import { outputT_or_F, parseByCellType, filterBiosamples, assayHoverInfo, constructURL } from "../lib/filter-helpers" //Need to go back and define the types in mainQueryParams object export default function MainResultsFilters(props: { mainQueryParams: MainQueryParams; byCellType: CellTypeData }) { @@ -70,261 +70,153 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa const [PLS, setPLS] = useState(props.mainQueryParams.PLS) const [TF, setTF] = useState(props.mainQueryParams.TF) - const router = useRouter() - - //IMPORTANT: This will wipe the current cCRE when Nishi puts it in. Need to talk to Nishi about deciding when/how to display the cCRE details - /** - * - * @param newBiosample optional, use if setting Biosample State and then immediately triggering router before re-render when the new state is accessible - * @returns A URL configured with filter information - */ - function constructURL(newBiosample?: { selected: boolean; biosample: string; tissue: string; summaryName: string }) { - //Assembly, Chromosome, Start, End - const urlBasics = `search?assembly=${props.mainQueryParams.assembly}&chromosome=${props.mainQueryParams.chromosome}&start=${props.mainQueryParams.start}&end=${props.mainQueryParams.end}` - - //Can probably get biosample down to one string, and extract other info when parsing byCellType - const biosampleFilters = `&Tissue=${outputT_or_F(Tissue)}&PrimaryCell=${outputT_or_F(PrimaryCell)}&InVitro=${outputT_or_F( - InVitro - )}&Organoid=${outputT_or_F(Organoid)}&CellLine=${outputT_or_F(CellLine)}${ - (Biosample.selected && !newBiosample) || (newBiosample && newBiosample.selected) - ? "&Biosample=" + - (newBiosample ? newBiosample.biosample : Biosample.biosample) + - "&BiosampleTissue=" + - (newBiosample ? newBiosample.tissue : Biosample.tissue) + - "&BiosampleSummary=" + - (newBiosample ? newBiosample.summaryName : Biosample.summaryName) - : "" - }` - - const chromatinFilters = `&dnase_s=${DNaseStart}&dnase_e=${DNaseEnd}&h3k4me3_s=${H3K4me3Start}&h3k4me3_e=${H3K4me3End}&h3k27ac_s=${H3K27acStart}&h3k27ac_e=${H3K27acEnd}&ctcf_s=${CTCFStart}&ctcf_e=${CTCFEnd}` - - const classificationFilters = `&CA=${outputT_or_F(CA)}&CA_CTCF=${outputT_or_F(CA_CTCF)}&CA_H3K4me3=${outputT_or_F( - CA_H3K4me3 - )}&CA_TF=${outputT_or_F(CA_TF)}&dELS=${outputT_or_F(dELS)}&pELS=${outputT_or_F(pELS)}&PLS=${outputT_or_F(PLS)}&TF=${outputT_or_F(TF)}` - - const url = `${urlBasics}${biosampleFilters}${chromatinFilters}${classificationFilters}` - return url + const urlParams = { + Tissue, + PrimaryCell, + InVitro, + Organoid, + CellLine, + Biosample: { selected: Biosample.selected, biosample: Biosample.biosample, tissue: Biosample.tissue, summaryName: Biosample.summaryName }, + DNaseStart, + DNaseEnd, + H3K4me3Start, + H3K4me3End, + H3K27acStart, + H3K27acEnd, + CTCFStart, + CTCFEnd, + CA, + CA_CTCF, + CA_H3K4me3, + CA_TF, + dELS, + pELS, + PLS, + TF } - /** - * @param experiments Array of objects containing biosample experiments for a given biosample type - * @returns an object with keys dnase, atac, h3k4me3, h3k27ac, ctcf with each marked true or false - */ - function availableAssays( - experiments: { - assay: string - biosample_summary: string - biosample_type: string - tissue: string - value: string - }[] - ) { - const assays = { dnase: false, atac: false, h3k4me3: false, h3k27ac: false, ctcf: false } - experiments.forEach((exp) => (assays[exp.assay.toLowerCase()] = true)) - return assays - } - - /** - * - * @param byCellType JSON of byCellType - * @returns an object of sorted biosample types, grouped by tissue type - */ - function parseByCellType(byCellType: CellTypeData): UnfilteredBiosampleData { - const biosamples = {} - Object.entries(byCellType.byCellType).forEach((entry) => { - // if the tissue catergory hasn't been catalogued, make a new blank array for it - const experiments = entry[1] - var tissueArr = [] - if (!biosamples[experiments[0].tissue]) { - Object.defineProperty(biosamples, experiments[0].tissue, { - value: [], - enumerable: true, - writable: true, - }) - } - //The existing tissues - tissueArr = biosamples[experiments[0].tissue] - tissueArr.push({ - //display name - summaryName: experiments[0].biosample_summary, - //for filtering - biosampleType: experiments[0].biosample_type, - //for query - queryValue: experiments[0].value, - //for filling in available assay wheels - //THIS DATA IS MISSING ATAC DATA! ATAC will always be false - assays: availableAssays(experiments), - //for displaying tissue category when selected - biosampleTissue: experiments[0].tissue, - }) - Object.defineProperty(biosamples, experiments[0].tissue, { value: tissueArr, enumerable: true, writable: true }) - }) - return biosamples - } - - function assayHoverInfo(assays: { dnase: boolean; h3k27ac: boolean; h3k4me3; ctcf: boolean; atac: boolean }) { - const dnase = assays.dnase - const h3k27ac = assays.h3k27ac - const h3k4me3 = assays.h3k4me3 - const ctcf = assays.ctcf - const atac = assays.atac - - if (dnase && h3k27ac && h3k4me3 && ctcf && atac) { - return "All assays available" - } else if (!dnase && !h3k27ac && !h3k4me3 && !ctcf && !atac) { - return "No assays available" - } else - return `Available:\n${dnase ? "DNase\n" : ""}${h3k27ac ? "H3K27ac\n" : ""}${h3k4me3 ? "H3K4me3\n" : ""}${ctcf ? "CTCF\n" : ""}${ - atac ? "ATAC\n" : "" - }` - } - - /** - * - * @param biosamples The biosamples object to filter - * @returns The same object but filtered with the current state of Biosample Type filters - */ - function filterBiosamples(biosamples: UnfilteredBiosampleData): FilteredBiosampleData { - const filteredBiosamples: FilteredBiosampleData = Object.entries(biosamples).map(([str, objArray]) => [ - str, - objArray.filter((biosample) => { - if (Tissue && biosample.biosampleType === "tissue") { - return true - } else if (PrimaryCell && biosample.biosampleType === "primary cell") { - return true - } else if (CellLine && biosample.biosampleType === "cell line") { - return true - } else if (InVitro && biosample.biosampleType === "in vitro differentiated cells") { - return true - } else if (Organoid && biosample.biosampleType === "organoid") { - return true - } else return false - }), - ]) - return filteredBiosamples - } + const router = useRouter() - //This is being called every time the state is changed, which is problematic. Need to have it be called only once, or on change to biosample filter state. Instant checkboxes rely on atomatically running this. Of note, it runs twice? /** - * @returns MUI Accordion components populated with a table of each tissue's biosamples + * Biosample Tables, only re-rendered if the relevant state variables change. Prevents sluggish sliders in other filters */ - function generateBiosampleTables() { - const filteredBiosamples: FilteredBiosampleData = filterBiosamples(parseByCellType(props.byCellType)) - const cols = [ - { - header: "Biosample", - value: (row) => row.summaryName, - render: (row) => ( - - {row.summaryName} - - ), - }, - { - header: "Assays", - value: (row) => Object.keys(row.assays).filter((key) => row.assays[key] === true).length, - render: (row) => { - const fifth = (2 * 3.1416 * 10) / 5 - return ( - - - - - - - - - + const biosampleTables = useMemo( + () => { + const filteredBiosamples: FilteredBiosampleData = filterBiosamples(parseByCellType(props.byCellType), Tissue, PrimaryCell, CellLine, InVitro, Organoid) + const cols = [ + { + header: "Biosample", + value: (row) => row.summaryName, + render: (row) => ( + + {row.summaryName} - ) + ), + }, + { + header: "Assays", + value: (row) => Object.keys(row.assays).filter((key) => row.assays[key] === true).length, + render: (row) => { + const fifth = (2 * 3.1416 * 10) / 5 + return ( + + + + + + + + + + + ) + }, }, - }, - ] + ] - return filteredBiosamples.sort().map((tissue: [string, {}[]], i) => { - // If user enters a search, check to see if the tissue name matches - if (tissue[0].includes(SearchString)) { - return ( - - } - sx={{ - flexDirection: "row-reverse", - "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": { - transform: "rotate(90deg)", - }, - }} - > - {tissue[0][0].toUpperCase() + tissue[0].slice(1)} - - - { - setBiosample({ selected: true, biosample: row.queryValue, tissue: row.biosampleTissue, summaryName: row.summaryName }) - setBiosampleHighlight(row) - //Push to router with new biosample to avoid accessing stale Biosample value - router.push( - constructURL({ selected: true, biosample: row.queryValue, tissue: row.biosampleTissue, summaryName: row.summaryName }) - ) + return filteredBiosamples.sort().map((tissue: [string, {}[]], i) => { + // If user enters a search, check to see if the tissue name matches + if (tissue[0].includes(SearchString)) { + return ( + + } + sx={{ + flexDirection: "row-reverse", + "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": { + transform: "rotate(90deg)", + }, }} - /> - - - ) - } - }) - } - - //Only trigger re-render of the tables if the relevant state variables change. Prevents sluggish sliders in other filters - const biosampleTables = useMemo( - () => generateBiosampleTables(), - [CellLine, PrimaryCell, Tissue, Organoid, InVitro, Biosample, BiosampleHighlight, SearchString, generateBiosampleTables] + > + {tissue[0][0].toUpperCase() + tissue[0].slice(1)} + + + { + setBiosample({ selected: true, biosample: row.queryValue, tissue: row.biosampleTissue, summaryName: row.summaryName }) + setBiosampleHighlight(row) + //Push to router with new biosample to avoid accessing stale Biosample value + router.push( + constructURL(props.mainQueryParams, urlParams, { selected: true, biosample: row.queryValue, tissue: row.biosampleTissue, summaryName: row.summaryName }) + ) + }} + /> + + + ) + } + }) + } + , + // Linter wants to include biosampleTables here as a dependency. Including it breaks intended functionality. Revisit later? + // eslint-disable-next-line react-hooks/exhaustive-deps + [CellLine, InVitro, Organoid, PrimaryCell, Tissue, BiosampleHighlight, SearchString, props.byCellType, props.mainQueryParams] ) //Need to make this more responsive @@ -346,7 +238,7 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa Tissue/Organ - setSearchString(event.target.value)} /> + setSearchString(event.target.value)} /> {Biosample.selected && ( @@ -363,7 +255,7 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa onClick={() => { setBiosample({ selected: false, biosample: null, tissue: null, summaryName: null }) setBiosampleHighlight(null) - router.push(constructURL({ selected: false, biosample: null, tissue: null, summaryName: null })) + router.push(constructURL(props.mainQueryParams, urlParams, { selected: false, biosample: null, tissue: null, summaryName: null })) }} > Clear @@ -587,7 +479,7 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa - + diff --git a/screen2.0/src/common/components/MainResultsTable.tsx b/screen2.0/src/common/components/MainResultsTable.tsx index c820f5f9..781c9e7c 100644 --- a/screen2.0/src/common/components/MainResultsTable.tsx +++ b/screen2.0/src/common/components/MainResultsTable.tsx @@ -4,7 +4,6 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation" import { DataTable, DataTableProps, DataTableColumn } from "@weng-lab/psychscreen-ui-components" import React from "react" import { Typography } from "@mui/material" -import Link from "next/link" let COLUMNS = (rows) => { let col: DataTableColumn[] = [ @@ -34,25 +33,25 @@ let COLUMNS = (rows) => { }, ] - if (rows && rows[0].dnase !== null) { + if (rows[0] && rows[0].dnase !== null) { col.push({ header: "DNase", value: (row) => (row.dnase && row.dnase.toFixed(2)) || 0, }) } - if (rows && rows[0].ctcf !== null) { + if (rows[0] && rows[0].ctcf !== null) { col.push({ header: "CTCF", value: (row) => (row.ctcf && row.ctcf.toFixed(2)) || 0, }) } - if (rows && rows[0].h3k27ac != null) { + if (rows[0] && rows[0].h3k27ac != null) { col.push({ header: "H3K27ac", value: (row) => (row.h3k27ac && row.h3k27ac.toFixed(2)) || 0, }) } - if (rows && rows[0].h3k4me3 != null) { + if (rows[0] && rows[0].h3k4me3 != null) { col.push({ header: "H3K4me3", value: (row) => (row.h3k4me3 && row.h3k4me3.toFixed(2)) || 0, @@ -95,7 +94,7 @@ function MainResultsTable(props: Partial>) { return ( (assays[exp.assay.toLowerCase()] = true)) + return assays +} + +/** + * + * @param byCellType JSON of byCellType + * @returns an object of sorted biosample types, grouped by tissue type + */ +export function parseByCellType(byCellType: CellTypeData): UnfilteredBiosampleData { + const biosamples = {} + Object.entries(byCellType.byCellType).forEach((entry) => { + // if the tissue catergory hasn't been catalogued, make a new blank array for it + const experiments = entry[1] + var tissueArr = [] + if (!biosamples[experiments[0].tissue]) { + Object.defineProperty(biosamples, experiments[0].tissue, { + value: [], + enumerable: true, + writable: true, + }) + } + //The existing tissues + tissueArr = biosamples[experiments[0].tissue] + tissueArr.push({ + //display name + summaryName: experiments[0].biosample_summary, + //for filtering + biosampleType: experiments[0].biosample_type, + //for query + queryValue: experiments[0].value, + //for filling in available assay wheels + //THIS DATA IS MISSING ATAC DATA! ATAC will always be false + assays: availableAssays(experiments), + //for displaying tissue category when selected + biosampleTissue: experiments[0].tissue, + }) + Object.defineProperty(biosamples, experiments[0].tissue, { value: tissueArr, enumerable: true, writable: true }) + }) + return biosamples +} + +/** + * + * @param biosamples The biosamples object to filter + * @returns The same object but filtered with the current state of Biosample Type filters + */ +export function filterBiosamples(biosamples: UnfilteredBiosampleData, Tissue: boolean, PrimaryCell: boolean, CellLine: boolean, InVitro: boolean, Organoid: boolean,): FilteredBiosampleData { + const filteredBiosamples: FilteredBiosampleData = Object.entries(biosamples).map(([str, objArray]) => [ + str, + objArray.filter((biosample) => { + if (Tissue && biosample.biosampleType === "tissue") { + return true + } else if (PrimaryCell && biosample.biosampleType === "primary cell") { + return true + } else if (CellLine && biosample.biosampleType === "cell line") { + return true + } else if (InVitro && biosample.biosampleType === "in vitro differentiated cells") { + return true + } else if (Organoid && biosample.biosampleType === "organoid") { + return true + } else return false + }), + ]) + return filteredBiosamples +} + +export function assayHoverInfo(assays: { dnase: boolean; h3k27ac: boolean; h3k4me3: any; ctcf: boolean; atac: boolean }) { + const dnase = assays.dnase + const h3k27ac = assays.h3k27ac + const h3k4me3 = assays.h3k4me3 + const ctcf = assays.ctcf + const atac = assays.atac + + if (dnase && h3k27ac && h3k4me3 && ctcf && atac) { + return "All assays available" + } else if (!dnase && !h3k27ac && !h3k4me3 && !ctcf && !atac) { + return "No assays available" + } else + return `Available:\n${dnase ? "DNase\n" : ""}${h3k27ac ? "H3K27ac\n" : ""}${h3k4me3 ? "H3K4me3\n" : ""}${ctcf ? "CTCF\n" : ""}${atac ? "ATAC\n" : "" + }` +} + +//IMPORTANT: This will wipe the current cCRE when Nishi puts it in. Need to talk to Nishi about deciding when/how to display the cCRE details +/** + * + * @param newBiosample optional, use if setting Biosample State and then immediately triggering router before re-render when the new state is accessible + * @returns A URL configured with filter information + */ +export function constructURL( + mainQueryParams: MainQueryParams, + urlParams: { + Tissue: boolean, + PrimaryCell: boolean, + InVitro: boolean, + Organoid: boolean, + CellLine: boolean, + Biosample: { selected: boolean; biosample: string | null; tissue: string | null; summaryName: string | null }, + DNaseStart: number, + DNaseEnd: number, + H3K4me3Start: number, + H3K4me3End: number, + H3K27acStart: number, + H3K27acEnd: number, + CTCFStart: number, + CTCFEnd: number, + CA: boolean, + CA_CTCF: boolean, + CA_H3K4me3: boolean, + CA_TF: boolean, + dELS: boolean, + pELS: boolean, + PLS: boolean, + TF: boolean + }, + newBiosample?: { + selected: boolean; + biosample: string; + tissue: string; + summaryName: string + } +) { + //Assembly, Chromosome, Start, End + const urlBasics = `search?assembly=${mainQueryParams.assembly}&chromosome=${mainQueryParams.chromosome}&start=${mainQueryParams.start}&end=${mainQueryParams.end}` + + //Can probably get biosample down to one string, and extract other info when parsing byCellType + const biosampleFilters = `&Tissue=${outputT_or_F(urlParams.Tissue)}&PrimaryCell=${outputT_or_F(urlParams.PrimaryCell)}&InVitro=${outputT_or_F( + urlParams.InVitro + )}&Organoid=${outputT_or_F(urlParams.Organoid)}&CellLine=${outputT_or_F(urlParams.CellLine)}${(urlParams.Biosample.selected && !newBiosample) || (newBiosample && newBiosample.selected) + ? "&Biosample=" + + (newBiosample ? newBiosample.biosample : urlParams.Biosample.biosample) + + "&BiosampleTissue=" + + (newBiosample ? newBiosample.tissue : urlParams.Biosample.tissue) + + "&BiosampleSummary=" + + (newBiosample ? newBiosample.summaryName : urlParams.Biosample.summaryName) + : "" + }` + + const chromatinFilters = `&dnase_s=${urlParams.DNaseStart}&dnase_e=${urlParams.DNaseEnd}&h3k4me3_s=${urlParams.H3K4me3Start}&h3k4me3_e=${urlParams.H3K4me3End}&h3k27ac_s=${urlParams.H3K27acStart}&h3k27ac_e=${urlParams.H3K27acEnd}&ctcf_s=${urlParams.CTCFStart}&ctcf_e=${urlParams.CTCFEnd}` + + const classificationFilters = `&CA=${outputT_or_F(urlParams.CA)}&CA_CTCF=${outputT_or_F(urlParams.CA_CTCF)}&CA_H3K4me3=${outputT_or_F( + urlParams.CA_H3K4me3 + )}&CA_TF=${outputT_or_F(urlParams.CA_TF)}&dELS=${outputT_or_F(urlParams.dELS)}&pELS=${outputT_or_F(urlParams.pELS)}&PLS=${outputT_or_F(urlParams.PLS)}&TF=${outputT_or_F(urlParams.TF)}` + + const url = `${urlBasics}${biosampleFilters}${chromatinFilters}${classificationFilters}` + return url +} \ No newline at end of file diff --git a/screen2.0/yarn.lock b/screen2.0/yarn.lock index 27ab2acf..d370a53a 100644 --- a/screen2.0/yarn.lock +++ b/screen2.0/yarn.lock @@ -7438,11 +7438,11 @@ __metadata: "typescript@patch:typescript@5.1.6#~builtin": version: 5.1.6 - resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=5da071" + resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=85af82" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: f53bfe97f7c8b2b6d23cf572750d4e7d1e0c5fff1c36d859d0ec84556a827b8785077bc27676bf7e71fae538e517c3ecc0f37e7f593be913d884805d931bc8be + checksum: 21e88b0a0c0226f9cb9fd25b9626fb05b4c0f3fddac521844a13e1f30beb8f14e90bd409a9ac43c812c5946d714d6e0dee12d5d02dfc1c562c5aacfa1f49b606 languageName: node linkType: hard