From d26828476992e47b95f3410641e9eafcdfe6329d Mon Sep 17 00:00:00 2001 From: Jordan <110398589+jordanvelezbomba@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:23:18 -0400 Subject: [PATCH 1/2] Gene expression app & RAMPAGE tab (#76) * initial upload, added fetch, buttons, title * added types for gene exp * added expression plot, fixed buttons, and added gene autocomplete * fixed gene autocomplete * added rampage tab and adjusted spacing * fixed scaling and adjusted dimensions * changed tab name * added sort, zero switch, and transcript select * formatted numbers * spacing, prettier * change rampage tab name * added dropdowns to graph, added drawer, and fixed scaling * added settings * replaced details tab with hamburger * updated error logger * hyperlinked genes in main table * added gene expression tab to details * upated types * added collapse all, removed outline, and fixed scaling * added toolbar, added url nav, fixed search, and added collapse all * added header and updated descriptions * adjusted buttons / spacing, fixed plot scaling, and added collapse all * prettier * fixed link pathname and error utility * added keys, cleaned types * fixed hamburger height and width, cleaned types * changed error util * changed default export for build * moved gene-expression tab * fixed default export * fixed error handler * rerun checks * changed header color * added theme * fixed collapse all, changed theme * added all track colors * updated theme * updated theme * updated theme * cleaned up code, moved utils * replaced autocomplete with select * cleaned up * updated collapse all --- .../differential-gene-expression/page.tsx | 34 +- .../gene-expression/gene-autocomplete.tsx | 177 +++++++++ .../app/applets/gene-expression/options.tsx | 193 ++++++++++ .../src/app/applets/gene-expression/page.tsx | 259 +++++++++++++ .../src/app/applets/gene-expression/types.ts | 109 ++++++ .../src/app/applets/gene-expression/utils.tsx | 155 ++++++++ screen2.0/src/app/applets/gwas/page.tsx | 8 +- screen2.0/src/app/error.tsx | 2 +- screen2.0/src/app/global-error.tsx | 2 +- .../app/search/ccredetails/ccredetails.tsx | 212 ++++++++--- .../search/ccredetails/gene-expression.tsx | 266 +++++++++++++ .../app/search/ccredetails/linkedccres.tsx | 4 +- .../ccredetails/nearbygenomicfeatures.tsx | 7 +- .../src/app/search/ccredetails/rampage.tsx | 166 ++++++++ .../src/app/search/ccredetails/utils.tsx | 217 +++++++++++ screen2.0/src/app/search/ccresearch.tsx | 9 +- .../common/components/MainResultsFilters.tsx | 44 ++- .../common/components/MainResultsTable.tsx | 53 ++- .../common/components/ResponsiveAppBar.tsx | 354 +++++++++++------- screen2.0/src/common/lib/colors.ts | 59 ++- screen2.0/src/common/lib/filter-helpers.ts | 113 +++--- screen2.0/src/common/lib/themes.ts | 48 +++ screen2.0/src/common/lib/utility.tsx | 43 ++- screen2.0/types/types.ts | 2 + 24 files changed, 2219 insertions(+), 317 deletions(-) create mode 100644 screen2.0/src/app/applets/gene-expression/gene-autocomplete.tsx create mode 100644 screen2.0/src/app/applets/gene-expression/options.tsx create mode 100644 screen2.0/src/app/applets/gene-expression/page.tsx create mode 100644 screen2.0/src/app/applets/gene-expression/types.ts create mode 100644 screen2.0/src/app/applets/gene-expression/utils.tsx create mode 100644 screen2.0/src/app/search/ccredetails/gene-expression.tsx create mode 100644 screen2.0/src/app/search/ccredetails/rampage.tsx create mode 100644 screen2.0/src/common/lib/themes.ts diff --git a/screen2.0/src/app/applets/differential-gene-expression/page.tsx b/screen2.0/src/app/applets/differential-gene-expression/page.tsx index 282ee46c..dd58008e 100644 --- a/screen2.0/src/app/applets/differential-gene-expression/page.tsx +++ b/screen2.0/src/app/applets/differential-gene-expression/page.tsx @@ -88,7 +88,7 @@ export default function DifferentialGeneExpression() { if (!response.ok) { // throw new Error(response.statusText) setError(true) - return ErrorMessage(new Error(response.statusText)) + return } return response.json() }) @@ -99,7 +99,7 @@ export default function DifferentialGeneExpression() { .catch((error: Error) => { // logging // throw error - return ErrorMessage(error) + return }) setLoading(true) }, []) @@ -124,7 +124,7 @@ export default function DifferentialGeneExpression() { if (!response.ok) { // throw new Error(response.statusText) setError(true) - return ErrorMessage(new Error(response.statusText)) + return } return response.json() }) @@ -170,7 +170,7 @@ export default function DifferentialGeneExpression() { .catch((error: Error) => { // logging // throw error - return ErrorMessage(error) + return }) setLoadingChart(true) }, [ct1, ct2, gene]) @@ -243,7 +243,7 @@ export default function DifferentialGeneExpression() { // const cellTypes2 = await getCellTypes() return loading ? ( - LoadingMessage() + ) : ( @@ -291,9 +291,9 @@ export default function DifferentialGeneExpression() { {errorLoading - ? ErrorMessage(new Error("Error loading data")) + ? : loadingChart - ? LoadingMessage() + ? : data && data.gene && data[data.gene] && @@ -429,7 +429,7 @@ export default function DifferentialGeneExpression() { }) setSlider([dr1, range.x.end + 1200000]) setMin(dr1) - } else ErrorMessage(new Error("invalid range")) + } else return } }} /> @@ -459,7 +459,7 @@ export default function DifferentialGeneExpression() { }) setSlider([range.x.start - 1200000, dr2]) setMax(dr2) - } else ErrorMessage(new Error("invalid range")) + } else return } }} /> @@ -587,24 +587,24 @@ export default function DifferentialGeneExpression() { {!toggleFC ? ( <>> ) : ( - data[data.gene].nearbyDEs.data.map((point, i: number) => - BarPoint(point, i, range, dimensions) + data[data.gene].nearbyDEs.data.map( + (point, i: number) => BarPoint(point, i, range, dimensions) // ) )} {!toggleccres ? ( <>> ) : ( - data[data.gene].diffCREs.data.map((point, i: number) => - Point(point, i, range, dimensions) + data[data.gene].diffCREs.data.map( + (point, i: number) => Point(point, i, range, dimensions) // ) )} {!toggleGenes ? ( <>> ) : ( - data[data.gene].nearbyDEs.genes.map((point, i: number) => - GenePoint(point, i, range, dimensions, toggleGenes) + data[data.gene].nearbyDEs.genes.map( + (point, i: number) => GenePoint(point, i, range, dimensions, toggleGenes) // ) )} @@ -674,8 +674,8 @@ export default function DifferentialGeneExpression() { - {data[data.gene].nearbyDEs.genes.map((point, i: number) => - GenePoint(point, i, range, dimensions, false) + {data[data.gene].nearbyDEs.genes.map( + (point, i: number) => GenePoint(point, i, range, dimensions, false) // )} diff --git a/screen2.0/src/app/applets/gene-expression/gene-autocomplete.tsx b/screen2.0/src/app/applets/gene-expression/gene-autocomplete.tsx new file mode 100644 index 00000000..cdb4154b --- /dev/null +++ b/screen2.0/src/app/applets/gene-expression/gene-autocomplete.tsx @@ -0,0 +1,177 @@ +"use client" +import React, { useState, useEffect, useCallback } from "react" +import { useRouter } from "next/navigation" + +import { Autocomplete, TextField, Box, Button, debounce, Typography } from "@mui/material" + +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" +import { gene } from "./types" +import { QueryResponse } from "../../../../types/types" +import { Dispatch, SetStateAction } from "react" + +const GENE_AUTOCOMPLETE_QUERY = ` + query ($assembly: String!, $name_prefix: [String!], $limit: Int) { + gene(assembly: $assembly, name_prefix: $name_prefix, limit: $limit) { + name + id + coordinates { + start + chromosome + end + } + } + } +` + +export default function GeneAutoComplete(props: { + assembly: string + gene: string + pathname: string + setGene: Dispatch> +}) { + const router = useRouter() + + const [options, setOptions] = useState([]) + const [geneDesc, setgeneDesc] = useState<{ name: string; desc: string }[]>() + const [geneList, setGeneList] = useState([]) + const [geneID, setGeneID] = useState(props.gene ? props.gene : "OR51AB1P") + const [assembly, setAssembly] = useState(props.assembly) + // const [current_gene, setGene] = useState(props.gene ? props.gene : "OR51AB1P") + + // gene descriptions + useEffect(() => { + const fetchData = async () => { + let f = await Promise.all( + options.map((gene) => + fetch("https://clinicaltables.nlm.nih.gov/api/ncbi_genes/v3/search?authenticity_token=&terms=" + gene.toUpperCase()) + .then((x) => x && x.json()) + .then((x) => { + const matches = (x as QueryResponse)[3] && (x as QueryResponse)[3].filter((x) => x[3] === gene.toUpperCase()) + return { + desc: matches && matches.length >= 1 ? matches[0][4] : "(no description available)", + name: gene, + } + }) + .catch(() => { + return { desc: "(no description available)", name: gene } + }) + ) + ) + setgeneDesc(f) + } + + options && fetchData() + }, [options]) + + // gene list + const onSearchChange = async (value: string) => { + setOptions([]) + const response = await fetch("https://ga.staging.wenglab.org/graphql", { + method: "POST", + body: JSON.stringify({ + query: GENE_AUTOCOMPLETE_QUERY, + variables: { + assembly: props.assembly, + name_prefix: value, + limit: 100, + }, + }), + headers: { "Content-Type": "application/json" }, + }) + const genesSuggestion = (await response.json()).data?.gene + if (genesSuggestion && genesSuggestion.length > 0) { + const r = genesSuggestion.map((g) => g.name) + const g = genesSuggestion.map((g) => { + return { + chrom: g.coordinates.chromosome, + start: g.coordinates.start, + end: g.coordinates.end, + id: g.id, + name: g.name, + } + }) + setOptions(r) + setGeneList(g) + } else if (genesSuggestion && genesSuggestion.length === 0) { + setOptions([]) + setGeneList([]) + } + } + + // delay fetch + const debounceFn = useCallback(debounce(onSearchChange, 500), []) + + return ( + + , value: string) => { + if (value != "") debounceFn(value) + setGeneID(value) + }} + onInputChange={(event: React.ChangeEvent, value: string) => { + if (value != "") debounceFn(value) + setGeneID(value) + }} + onKeyDown={(e) => { + if (e.key == "Enter") { + for (let g of geneList) { + if (g.name === geneID && g.end - g.start > 0) { + props.setGene(g.name) + // replace url if ge applet + if (props.pathname.split("/").includes("gene-expression")) router.replace(props.pathname + "?gene=" + g.name) + break + } + } + } + }} + renderInput={(props) => } + renderOption={(props, opt) => { + return ( + + + + + {opt} + + {geneDesc && geneDesc.find((g) => g.name === opt) && ( + + {geneDesc.find((g) => g.name === opt)?.desc} + + )} + + + + ) + }} + /> + { + for (let g of geneList) { + if (g.name === geneID && g.end - g.start > 0) { + props.setGene(g.name) + // replace url if ge applet + if (props.pathname.split("/").includes("gene-expression")) router.replace(props.pathname + "?gene=" + g.name) + break + } + } + }} + color="primary" + > + Search + + + ) +} diff --git a/screen2.0/src/app/applets/gene-expression/options.tsx b/screen2.0/src/app/applets/gene-expression/options.tsx new file mode 100644 index 00000000..5b3d5460 --- /dev/null +++ b/screen2.0/src/app/applets/gene-expression/options.tsx @@ -0,0 +1,193 @@ +import React from "react" +import { + Accordion, + AccordionDetails, + AccordionSummary, + Checkbox, + FormControlLabel, + FormGroup, + ToggleButton, + ToggleButtonGroup, + Typography, +} from "@mui/material" + +import { ThemeProvider } from "@mui/material/styles" +import { CheckBox, ExpandMore } from "@mui/icons-material" +import ExpandMoreIcon from "@mui/icons-material/ExpandMore" + +// remove or add list of checked items +const toggleList = (checkList: string[], option: string) => { + if (checkList === undefined || checkList.length === 0) return [option] + if (checkList.includes(option)) { + const index = checkList.indexOf(option, 0) + if (index > -1) { + checkList.splice(index, 1) + } + } else { + checkList.push(option) + } + let toggleList: string[] = [] + Object.values(checkList).map((x: string) => toggleList.push(x)) + + return toggleList +} + +export const OptionsBiosampleTypes = (props: { + biosamples: string[], + setBiosamples: React.Dispatch> +}) => { + let labels: string[] = ["cell line", "in vitro differentiated cells", "primary cell", "tissue"] + return ( + + }> + Biosample Types + + + + {Object.values(labels).map((biosample: string) => ( + props.setBiosamples(toggleList(props.biosamples, biosample))} + /> + } + /> + ) + )} + + + + ) +} + +export const OptionsCellularComponents = (props: { + cell_components: string[], + setCellComponents: React.Dispatch> +}) => { + let labels: string[] = ["cell", "chromatin", "cytosol", "membrane", "nucleoplus", "nucleoplasm", "nucleus"] + return ( + + }> + Cellular Compartments + + + + {Object.values(labels).map((comp: string) => ( + props.setCellComponents(toggleList(props.cell_components, comp))} + /> + } + /> + ) + )} + + + + ) +} + +export const OptionsGroupBy = (props: { group: string, setGroup: React.Dispatch> }) => { + return ( + + }> + Group By + + + , value: string) => { + if (value !== props.group) props.setGroup(value) + }} + aria-label="Platform" + > + Experiment + Tissue + Tissue Max + + + + ) +} + +export const OptionsRNAType = (props: { RNAtype: string, setRNAType: React.Dispatch> }) => { + return ( + + }> + RNA Type + + + , value: string) => { + if (value !== props.RNAtype) props.setRNAType(value) + }} + aria-label="Platform" + > + Total RNA-seq + PolyA RNA-seq + Any + + + + ) +} + +export const OptionsScale = (props: { scale: string, setScale: React.Dispatch> }) => { + return ( + + }> + Scale + + + , value: string) => { + if (value !== props.scale) props.setScale(value) + }} + aria-label="Platform" + > + Linear + Log2 + + + + ) +} + +export const OptionsReplicates = (props: { replicates: string, setReplicates: React.Dispatch> }) => { + return ( + + }> + Replicates + + + , value: string) => { + if (value !== props.replicates) props.setReplicates(value) + }} + aria-label="Platform" + > + Average + Individual + + + + ) +} diff --git a/screen2.0/src/app/applets/gene-expression/page.tsx b/screen2.0/src/app/applets/gene-expression/page.tsx new file mode 100644 index 00000000..407c4760 --- /dev/null +++ b/screen2.0/src/app/applets/gene-expression/page.tsx @@ -0,0 +1,259 @@ +"use client" +import React, { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import { ReadonlyURLSearchParams, useSearchParams, usePathname } from "next/navigation" +import { LoadingMessage, ErrorMessage } from "../../../common/lib/utility" + +import { PlotGeneExpression } from "./utils" +import { GeneExpressions } from "./types" +import { Range2D } from "jubilant-carnival" + +import { Box, Button, Typography, IconButton, Drawer, Toolbar, AppBar, Stack, Paper, Switch } from "@mui/material" + +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" +import Divider from "@mui/material/Divider" +import { ThemeProvider } from "@mui/material/styles" +import { defaultTheme } from "../../../common/lib/themes" +import MenuIcon from "@mui/icons-material/Menu" +import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos" + +import Image from "next/image" +import GeneAutoComplete from "./gene-autocomplete" +import { + OptionsBiosampleTypes, + OptionsCellularComponents, + OptionsGroupBy, + OptionsRNAType, + OptionsReplicates, + OptionsScale, +} from "./options" + +export default function GeneExpression() { + const searchParams: ReadonlyURLSearchParams = useSearchParams()! + const router = useRouter() + const pathname = usePathname() + + if (!pathname.split("/").includes("search") && !searchParams.get("gene")) router.push(pathname + "?gene=OR51AB1P") + + const [loading, setLoading] = useState(true) + const [data, setData] = useState() + const [open, setState] = useState(true) + + const [current_assembly, setAssembly] = useState("GRCh38") + const [current_gene, setGene] = useState(searchParams.get("gene") ? searchParams.get("gene") : "OR51AB1P") + + const [biosamples, setBiosamples] = useState(["cell line", "in vitro differentiated cells", "primary cell", "tissue"]) + const [cell_components, setCellComponents] = useState(["cell"]) + + const [group, setGroup] = useState("byTissueMaxFPKM") // experiment, tissue, tissue max + const [RNAtype, setRNAType] = useState("all") // any, polyA RNA-seq, total RNA-seq + const [scale, setScale] = useState("rawFPKM") // linear or log2 + const [replicates, setReplicates] = useState("mean") // single or mean + + const [range, setRange] = useState({ + x: { start: 0, end: 4 }, + y: { start: 0, end: 0 }, + }) + + const [dimensions, setDimensions] = useState({ + x: { start: 125, end: 650 }, + y: { start: 250, end: 0 }, + }) + + // fetch gene expression data + useEffect(() => { + fetch("https://screen-beta-api.wenglab.org/gews/search", { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + assembly: current_assembly, + biosample_types_selected: biosamples, + compartments_selected: cell_components, + gene: current_gene, + }), + }) + .then((response) => { + if (!response.ok) { + return + } + return response.json() + }) + .then((data) => { + setData(data) + setLoading(false) + }) + .catch((error: Error) => { + return + }) + setLoading(true) + }, [current_assembly, current_gene, biosamples, cell_components]) + + const toggleDrawer = (open) => (event) => { + if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) { + return + } + setState(open) + } + + // set drawer height based on screen size + const drawerWidth: number = 350 + let drawerHeight: number = window.screen.height + let drawerHeightTab: number = window.screen.height + + // 1080 + if (drawerHeight < 1200) { + drawerHeight *= 0.85 + drawerHeightTab *= 0.6 + } // 2k + else if (drawerHeight < 2000) { + drawerHeight *= 0.9 + drawerHeightTab *= 0.7 + } // 4k + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + {current_gene} + + + {" "} + Gene Expression Profiles by RNA-seq + + + + + + + + + + + + + + + {/* */} + + + + + + + + {/* mouse switch - info? */} + + { + if (current_assembly === "mm10") setAssembly("GRCh38") + else setAssembly("mm10") + setGene("OR51AB1P") + }} + /> + mm10 + + {biosamples.length === 0 ? ( + + ) : cell_components.length === 0 ? ( + + ) : loading ? ( + + + + ) : ( + data && + data["all"] && + data["polyA RNA-seq"] && + data["total RNA-seq"] && ( + + ) + )} + + + + + ) +} diff --git a/screen2.0/src/app/applets/gene-expression/types.ts b/screen2.0/src/app/applets/gene-expression/types.ts new file mode 100644 index 00000000..fa18e5ac --- /dev/null +++ b/screen2.0/src/app/applets/gene-expression/types.ts @@ -0,0 +1,109 @@ +export type gene = { + chrom: string + start: number + end: number + id: string + name: string +} + +export type RIDItem = { + ageTitle: string + cellType: string + logFPKM: number + logTPM: number + rID: number + rawFPKM: number + rawTPM: number + rep: number + tissue: string +} + +export type RIDItemList = { + [id: string]: RIDItem +} + +export type GeneExpEntry = { + value: number + biosample_term?: string + cellType?: string + expID: string + rep?: number + tissue: string + strand?: string + color: string +} + +export type BiosampleList = { + cell_line: boolean + in_vitro: boolean + primary_cell: boolean + tissue: boolean +} + +export type CellComponents = { + cell: boolean + chromatin: boolean + cytosol: boolean + membrane: boolean + nucleolus: boolean + nucleoplasm: boolean + nucleus: boolean +} + +export type ExpEntry = { + [group: string]: { + [tissue: string]: { + color: string + displayName: string + items: number[] + name: string + } + } +} + +export type GeneExpressions = { + all: { + assembly: string + coords: { + chrom: string + start: number + stop: number + } + ensemblid_ver: string + gene: string + itemsByRID: RIDItemList + mean: ExpEntry + single: ExpEntry + strand: string + } + assembly: string + gene: string + "polyA RNA-seq": { + assembly: string + coords: { + chrom: string + start: number + stop: number + } + ensemblid_ver: string + gene: string + itemsByRID: RIDItemList + mean: ExpEntry + single: ExpEntry + strand: string + } + "total RNA-seq": { + assembly: string + coords: { + chrom: string + start: number + stop: number + } + ensemblid_ver: string + gene: string + itemsByRID: RIDItemList + mean: ExpEntry + single: ExpEntry + strand: string + } +} diff --git a/screen2.0/src/app/applets/gene-expression/utils.tsx b/screen2.0/src/app/applets/gene-expression/utils.tsx new file mode 100644 index 00000000..54bb6636 --- /dev/null +++ b/screen2.0/src/app/applets/gene-expression/utils.tsx @@ -0,0 +1,155 @@ +import React, { useState } from "react" +import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Stack, ToggleButton, Typography } from "@mui/material" +import { styled } from "@mui/material/styles" +import { RIDItemList, GeneExpEntry, GeneExpressions, BiosampleList, CellComponents } from "./types" +import { Fragment } from "react" +import { Range2D, Point2D, linearTransform2D } from "jubilant-carnival" +import ExpandMoreIcon from "@mui/icons-material/ExpandMore" +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" + +export const ToggleButtonMean = styled(ToggleButton)(() => ({ + "&.Mui-selected, &.Mui-selected:hover": { + color: "white", + backgroundColor: "blue", + }, +})) + +export function PlotGeneExpression(props: { + data: GeneExpressions + range: Range2D + dimensions: Range2D + RNAtype: string + group: string + scale: string + replicates: string +}) { + const [collapse, setCollapse] = useState<{ [id: string]: boolean }>({}) + + let itemsRID: RIDItemList = props.data[props.RNAtype]["itemsByRID"] + let tissues: { [id: string]: { sum: number; values: GeneExpEntry[] } } = {} // dict of ftissues + let p1: Point2D = { x: 0, y: 0 } + let max: number = 0 + + Object.values(props.data[props.RNAtype][props.replicates][props.group]).map((biosample) => { + Object.values(biosample["items"]).map((id: string) => { + if (!tissues[itemsRID[id]["tissue"]]) tissues[itemsRID[id]["tissue"]] = { sum: 0, values: [] } + if (tissues[itemsRID[id]["tissue"]]) { + tissues[itemsRID[id]["tissue"]].sum += itemsRID[id][props.scale] + tissues[itemsRID[id]["tissue"]].values.push({ + value: itemsRID[id][props.scale], + cellType: itemsRID[id]["cellType"], + expID: itemsRID[id]["expID"], + rep: itemsRID[id]["rep"], + tissue: itemsRID[id]["tissue"], + color: biosample["color"], + }) + } + + if (props.group === "byTissueMaxFPKM" && tissues[itemsRID[id]["tissue"]].sum > max) max = tissues[itemsRID[id]["tissue"]].sum + else if (itemsRID[id][props.scale] > max) max = itemsRID[id][props.scale] + }) + }) + + props.range.x.end = max + + // returns bar plot for a tissue + const plotGeneExp = (entry: any, index: number, y: number) => { + let tissue: string = entry[0] + let info: any = entry[1] + + return Object.values(info.values).map((item: GeneExpEntry, i: number) => { + p1 = linearTransform2D(props.range, props.dimensions)({ x: item.value, y: 0 }) + return ( + + + {item.value} + + + {Number(item.value.toFixed(3)) + " "} + {item.expID} + {" " + item.cellType} + + + + ) + }) + } + + let y: number = 0 + return ( + <> + + + { + let c: { [id: string]: boolean } = {} + let uncollapse: boolean = true + if (Object.keys(collapse).length !== 0) { + Object.keys(collapse).map((b: string) => { + if (collapse[b]) uncollapse = false + c[b] = false + }) + + if (uncollapse) { + Object.keys(collapse).map((b: string) => { + c[b] = true + }) + } + } else + Object.keys(tissues).map((b: string) => { + c[b] = false + }) + setCollapse(c) + }} + > + Collapse All + + + + + + {Object.entries(tissues).map((entry, index: number) => { + let info: any = entry[1] + y = info.values.length + 20 + 10 + let view: string = "0 0 1200 " + (info.values.length * 20 + 20) + return ( + + } + sx={{ padding: 0, margin: 0 }} + onClick={() => { + let tmp: { [id: string]: boolean } = {} + Object.entries(tissues).map((x) => { + if (x[0] === entry[0]) { + if (collapse[entry[0]] === undefined || collapse[entry[0]]) tmp[entry[0]] = false + else tmp[entry[0]] = true + } else { + tmp[x[0]] = (collapse[x[0]] !== undefined ? collapse[x[0]] : true) + } + }) + setCollapse(tmp) + }} + > + {entry[0]} + + + + + + {plotGeneExp(entry, index, 5)} + + + + + ) + })} + + + > + ) +} diff --git a/screen2.0/src/app/applets/gwas/page.tsx b/screen2.0/src/app/applets/gwas/page.tsx index 035ad24d..3e6ba146 100644 --- a/screen2.0/src/app/applets/gwas/page.tsx +++ b/screen2.0/src/app/applets/gwas/page.tsx @@ -32,7 +32,7 @@ export default function GWAS() { .then((response) => { if (!response.ok) { // throw new Error(response.statusText) - return ErrorMessage(new Error(response.statusText)) + return } return response.json() }) @@ -43,7 +43,7 @@ export default function GWAS() { .catch((error: Error) => { // logging // throw error - return ErrorMessage(error) + return }) setLoadingStudies(true) }, []) @@ -63,7 +63,7 @@ export default function GWAS() { .then((response) => { if (!response.ok) { // throw new Error(response.statusText) - return ErrorMessage(new Error(response.statusText)) + return } return response.json() }) @@ -74,7 +74,7 @@ export default function GWAS() { .catch((error: Error) => { // logging // throw error - return ErrorMessage(error) + return }) setLoadingStudy(true) }, [study]) diff --git a/screen2.0/src/app/error.tsx b/screen2.0/src/app/error.tsx index 8aefdd22..c3cf3c94 100644 --- a/screen2.0/src/app/error.tsx +++ b/screen2.0/src/app/error.tsx @@ -14,7 +14,7 @@ export default function Error({ error, reset }: { error: Error & { digest?: stri return ( - {ErrorMessage(error)} + - {ErrorMessage(error)} + = ({ accession, region, globals, assembly }) => { +export const CcreDetails: React.FC = ({ accession, region, globals, assembly, genes }) => { const [value, setValue] = React.useState(0) + const [open, setState] = React.useState(true) + const handleChange = (_, newValue: number) => { setValue(newValue) } + + const toggleDrawer = (open) => (event) => { + if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) { + return + } + setState(open) + } + + const drawerWidth: number = 350 + return ( - - - - - {accession} - - - - {`${region.chrom}:${region.start}-${region.end}`} - - - - - - - - - - - - - - { - - - {value === 0 && } - {value === 1 && } - {value === 2 && ( - - )} - {value === 3 && ( - - )} - {value === 5 && } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {accession} + + + + {`${region.chrom}:${region.start}-${region.end}`} + + + + + + + + {value === 0 && } + {value === 1 && } + {value === 2 && ( + + )} + {value === 3 && ( + + )} + {value === 5 && } + {value === 6 && } + {value === 7 && } + - } - - + + + ) } diff --git a/screen2.0/src/app/search/ccredetails/gene-expression.tsx b/screen2.0/src/app/search/ccredetails/gene-expression.tsx new file mode 100644 index 00000000..8a500e98 --- /dev/null +++ b/screen2.0/src/app/search/ccredetails/gene-expression.tsx @@ -0,0 +1,266 @@ +"use client" +import React, { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import { usePathname } from "next/navigation" +import { LoadingMessage, ErrorMessage } from "../../../common/lib/utility" + +import { PlotGeneExpression } from "../../applets/gene-expression/utils" +import { GeneExpressions } from "../../applets/gene-expression/types" +import { Range2D } from "jubilant-carnival" + +import { Box, Button, Typography, IconButton, Drawer, Toolbar, AppBar, Stack, Paper, TextField, MenuItem } from "@mui/material" + +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" +import Divider from "@mui/material/Divider" +import { ThemeProvider } from "@mui/material/styles" +import { defaultTheme } from "../../../common/lib/themes" +import MenuIcon from "@mui/icons-material/Menu" +import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos" + +import Image from "next/image" +import { + OptionsBiosampleTypes, + OptionsCellularComponents, + OptionsGroupBy, + OptionsRNAType, + OptionsReplicates, + OptionsScale, +} from "../../applets/gene-expression/options" + +export function GeneExpression(props: { + accession: string + assembly: string + genes: { pc: { name: string; __typename: string }[]; all: { name: string; __typename: string }[] } + hamburger: boolean +}) { + const pathname = usePathname() + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const [data, setData] = useState() + const [options, setOptions] = useState([]) + const [open, setState] = useState(true) + + const [current_assembly, setAssembly] = useState(props.assembly ? props.assembly : "GRCh38") + const [current_gene, setGene] = useState(props.genes.pc[0].name) + + const [biosamples, setBiosamples] = useState(["cell line", "in vitro differentiated cells", "primary cell", "tissue"]) + const [cell_components, setCellComponents] = useState(["cell"]) + + const [group, setGroup] = useState("byTissueMaxFPKM") // experiment, tissue, tissue max + const [RNAtype, setRNAType] = useState("all") // any, polyA RNA-seq, total RNA-seq + const [scale, setScale] = useState("rawFPKM") // linear or log2 + const [replicates, setReplicates] = useState("mean") // single or mean + + const [range, setRange] = useState({ + x: { start: 0, end: 4 }, + y: { start: 0, end: 0 }, + }) + + const [dimensions, setDimensions] = useState({ + x: { start: 125, end: 650 }, + y: { start: 250, end: 0 }, + }) + + // fetch gene expression data + useEffect(() => { + fetch("https://screen-beta-api.wenglab.org/gews/search", { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + assembly: current_assembly, + biosample_types_selected: biosamples, + compartments_selected: cell_components, + gene: current_gene, + }), + }) + .then((response) => { + if (!response.ok) { + setError(true) + return + } + return response.json() + }) + .then((data) => { + setData(data) + let geneList: string[] = [] + for (let g of props.genes.pc) if (!geneList.includes(g.name)) geneList.push(g.name) + for (let g of props.genes.all) if (!geneList.includes(g.name)) geneList.push(g.name) + setOptions(geneList) + setLoading(false) + }) + .catch((error: Error) => { + return + }) + setLoading(true) + }, [current_assembly, current_gene, biosamples, cell_components]) + + const toggleDrawer = (open) => (event) => { + if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) { + return + } + setState(open) + } + + // set drawer height based on window size + const drawerWidth: number = 350 + let drawerHeight: number = window.screen.height + let drawerHeightTab: number = window.screen.height + + // 1080 + if (drawerHeight < 1200) { + drawerHeight *= 0.85 + drawerHeightTab *= 0.6 + } // 2k + else if (drawerHeight < 2000) { + drawerHeight *= 0.9 + drawerHeightTab *= 0.7 + } // 4k + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + toggleDrawer(false)} + sx={{ + ...(open && { display: "none" }), + }} + > + + + + + + + {current_gene} + + + {" "} + Gene Expression Profiles by RNA-seq + + + + + + + + + + + + + + + + + + + + + {options.map((option: string) => { + return ( + setGene(option)}> + {option} + + ) + })} + + + {biosamples.length === 0 ? ( + + ) : cell_components.length === 0 ? ( + + ) : loading ? ( + + + + ) : ( + data && + data["all"] && + data["polyA RNA-seq"] && + data["total RNA-seq"] && ( + + ) + )} + + + + + ) +} diff --git a/screen2.0/src/app/search/ccredetails/linkedccres.tsx b/screen2.0/src/app/search/ccredetails/linkedccres.tsx index 190b6236..81432fb7 100644 --- a/screen2.0/src/app/search/ccredetails/linkedccres.tsx +++ b/screen2.0/src/app/search/ccredetails/linkedccres.tsx @@ -38,9 +38,9 @@ export const Ortholog = ({ accession, assembly }) => { } return loading ? ( - LoadingMessage() + ) : error ? ( - ErrorMessage(error) + ) : ( diff --git a/screen2.0/src/app/search/ccredetails/nearbygenomicfeatures.tsx b/screen2.0/src/app/search/ccredetails/nearbygenomicfeatures.tsx index 4b0a8483..3ec145f1 100644 --- a/screen2.0/src/app/search/ccredetails/nearbygenomicfeatures.tsx +++ b/screen2.0/src/app/search/ccredetails/nearbygenomicfeatures.tsx @@ -167,7 +167,12 @@ export const NearByGenomicFeatures: React.FC<{ header: "Accession", value: (row) => row.name, render: (row) => ( - router.push(pathname + "?" + createQueryString("accession", row.name))} variant="body2" color="primary"> + router.push(pathname + "?" + createQueryString("accession", row.name))} + variant="body2" + color="primary" + > {row.name} ), diff --git a/screen2.0/src/app/search/ccredetails/rampage.tsx b/screen2.0/src/app/search/ccredetails/rampage.tsx new file mode 100644 index 00000000..4c907a4f --- /dev/null +++ b/screen2.0/src/app/search/ccredetails/rampage.tsx @@ -0,0 +1,166 @@ +"use client" +import React, { useState, useEffect } from "react" +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" +import { LoadingMessage, ErrorMessage } from "../../../common/lib/utility" +import { + AppBar, + Box, + Button, + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + ThemeProvider, + Toolbar, + Typography, + createTheme, +} from "@mui/material" + +import { Range2D } from "jubilant-carnival" +import { PlotActivityProfiles } from "./utils" +import Image from "next/image" +import { defaultTheme } from "../../../common/lib/themes" + +export default function Rampage(props: { accession: string; assembly: string; chromosome: string }) { + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const [data, setData] = useState() + const [transcript, setTranscript] = useState("") + + const [payload, setPayload] = useState<{ accession: string; assembly: string; chromosome: string }>({ + accession: props.accession, + assembly: props.assembly, + chromosome: props.chromosome, + }) + + const [range, setRange] = useState({ + x: { start: 0, end: 4 }, + y: { start: 0, end: 0 }, + }) + + const [dimensions, setDimensions] = useState({ + x: { start: 125, end: 650 }, + y: { start: 4900, end: 100 }, + }) + + // fetch rampage data + useEffect(() => { + fetch("https://screen-beta-api.wenglab.org/dataws/re_detail/rampage", { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + accession: payload.accession, + assembly: payload.assembly, + chromosome: payload.chromosome, + }), + }) + .then((response) => { + if (!response.ok) { + setError(true) + return + } + return response.json() + }) + .then((data) => { + setData(data) + setTranscript(data[payload.accession]["sortedTranscripts"][0]) + setLoading(false) + }) + .catch((error: Error) => { + return + }) + setLoading(true) + }, [payload]) + + function transcriptItems(transcripts: string[]) { + return Object.values(transcripts).map((t: string) => { + return ( + + {t} + + ) + }) + } + + return error ? ( + + ) : loading ? ( + + ) : ( + data && + data[payload.accession] && ( + + + + + + + + TSS Activity Profiles by RAMPAGE + + + + {data[payload.accession]["gene"]["name"]} + + {data[payload.accession]["gene"]["ensemblid_ver"] + + " (" + + parseInt(data[payload.accession]["gene"]["distance"]).toLocaleString("en-US") + + " bases from cCRE)"} + + + + + {"Transcript: "}{" "} + + + + { + setTranscript(event.target.value) + }} + > + {transcriptItems(data[payload.accession]["sortedTranscripts"])} + + + + {payload.chromosome + + ":" + + parseInt(data[payload.accession]["gene"]["start"]).toLocaleString("en-US") + + "-" + + parseInt(data[payload.accession]["gene"]["stop"]).toLocaleString("en-US") + + " unprocessed pseudogene"} + + + + + + + + + + + + + + + + + + + ) + ) +} diff --git a/screen2.0/src/app/search/ccredetails/utils.tsx b/screen2.0/src/app/search/ccredetails/utils.tsx index ff1fdddf..5b854556 100644 --- a/screen2.0/src/app/search/ccredetails/utils.tsx +++ b/screen2.0/src/app/search/ccredetails/utils.tsx @@ -1,3 +1,220 @@ +"use client" +import React, { useState } from "react" +import { Accordion, AccordionDetails, AccordionSummary } from "@mui/material" +import { + Box, + Button, + FormControl, + FormControlLabel, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + Switch, + Typography, +} from "@mui/material" +import { Point2D, Range2D, linearTransform2D } from "jubilant-carnival" +import { Fragment } from "react" +import ExpandMoreIcon from "@mui/icons-material/ExpandMore" +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" +import { RIDItemList, GeneExpEntry } from "../../applets/gene-expression/types" + +/** + * Plots associated RAMPAGE signals + * @param {any} data signals to plot + * @param {Range2D} range size of plot dimensions + * @param {Range2D} dimensions size of window to plot on + * @returns plot of RAMPAGE signals + */ +export function PlotActivityProfiles(props: { data: any; range: Range2D; dimensions: Range2D }) { + const [sort, setSort] = useState("byValue") + const [zeros, setZeros] = useState(false) + const [collapse, setCollapse] = useState<{ [id: string]: boolean }>({}) + + let transcripts: string[] = props.data["sortedTranscripts"] + let itemsRID: RIDItemList = props.data["tsss"][transcripts[0]]["itemsByID"] + let tissues: { [id: string]: { sum: number; values: GeneExpEntry[] } } = {} // dict of ftissues + let p1: Point2D = { x: 0, y: 0 } + let max: number = 0 + + Object.values(props.data["tsss"][transcripts[0]]["itemsGrouped"][sort]).map((biosample) => { + Object.values(biosample["items"]).map((id: string) => { + if (!zeros && itemsRID[id]["counts"] === 0) return + if (!tissues[biosample["tissue"]]) tissues[biosample["tissue"]] = { sum: 0, values: [] } + tissues[biosample["tissue"]].sum += itemsRID[id]["counts"] + tissues[biosample["tissue"]].values.push({ + value: itemsRID[id]["counts"], + biosample_term: itemsRID[id]["biosample_term_name"], + expID: itemsRID[id]["expid"], + tissue: biosample["tissue"], + strand: itemsRID[id]["strand"], + color: biosample["color"], + }) + + if (sort === "byTissueMax" && tissues[biosample["tissue"]].sum > max) max = tissues[biosample["tissue"]].sum + else if (itemsRID[id]["counts"] > max) max = itemsRID[id]["counts"] + }) + }) + + props.range.x.end = max + + // returns bar plot for a tissue + const plotGeneExp = (entry: any, index: number, y: number) => { + let tissue: string = entry[0] + let info: any = entry[1] + + return Object.values(info.values).map((item: any, i: number) => { + p1 = linearTransform2D(props.range, props.dimensions)({ x: item.value, y: 0 }) + return ( + + { + { + ; + } + }} + > + + + {item.value} + + + + {Number(item.value.toFixed(3)) + " "} + {item.expID} + {" " + item.biosample_term} + {" (" + item.strand + ")"} + + + + ) + }) + } + + let y: number = 0 + return ( + <> + + + + + Sort By + + { + setSort(event.target.value) + }} + size="small" + > + Tissue + Tissue Max + Value + + + + + + + { + let c: { [id: string]: boolean } = {} + let uncollapse: boolean = true + if (Object.keys(collapse).length !== 0) { + Object.keys(collapse).map((b: string) => { + if (collapse[b]) uncollapse = false + c[b] = false + }) + + if (uncollapse) { + Object.keys(collapse).map((b: string) => { + c[b] = true + }) + } + } else + Object.keys(tissues).map((b: string) => { + c[b] = false + }) + setCollapse(c) + }} + > + Collapse All + + + + + { + if (zeros) setZeros(false) + else setZeros(true) + }} + /> + } + label="display 0's" + /> + + + + {/* rampage plot */} + + + {Object.entries(tissues).map((entry, index: number) => { + let info: any = entry[1] + y += info.values.length * 20 + 20 + 25 + let view: string = "0 0 1200 " + (info.values.length * 20 + 20) + return ( + + } + sx={{ padding: 0, margin: 0 }} + onClick={() => { + let tmp: { [id: string]: boolean } = {} + Object.entries(tissues).map((x) => { + if (x[0] === entry[0]) { + if (collapse[entry[0]] === undefined || collapse[entry[0]]) tmp[entry[0]] = false + else tmp[entry[0]] = true + } else { + tmp[x[0]] = (collapse[x[0]] !== undefined ? collapse[x[0]] : true) + } + }) + setCollapse(tmp) + }} + > + {entry[0]} + + + + + + {plotGeneExp(entry, index, 5)} + + + + + ) + })} + + + > + ) +} + export const z_score = (d: any) => (d === -11.0 || d === "--" || d === undefined ? "--" : d.toFixed(2)) export const ctgroup = (group: string) => { diff --git a/screen2.0/src/app/search/ccresearch.tsx b/screen2.0/src/app/search/ccresearch.tsx index 868e0e9e..d49f3ac8 100644 --- a/screen2.0/src/app/search/ccresearch.tsx +++ b/screen2.0/src/app/search/ccresearch.tsx @@ -28,6 +28,7 @@ export const CcreSearch = ({ mainQueryParams, ccrerows, globals, assembly }) => //Need meaningful variable names please let f = ccrerows.find((c) => c.accession === searchParams.get("accession")) const region = { start: f?.start, chrom: f?.chromosome, end: f?.end } + console.log(f) return ( <> @@ -58,7 +59,13 @@ export const CcreSearch = ({ mainQueryParams, ccrerows, globals, assembly }) => {value === 1 && ( - + )} diff --git a/screen2.0/src/common/components/MainResultsFilters.tsx b/screen2.0/src/common/components/MainResultsFilters.tsx index d06cc30e..4cf02e2a 100644 --- a/screen2.0/src/common/components/MainResultsFilters.tsx +++ b/screen2.0/src/common/components/MainResultsFilters.tsx @@ -76,7 +76,12 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa InVitro, Organoid, CellLine, - Biosample: { selected: Biosample.selected, biosample: Biosample.biosample, tissue: Biosample.tissue, summaryName: Biosample.summaryName }, + Biosample: { + selected: Biosample.selected, + biosample: Biosample.biosample, + tissue: Biosample.tissue, + summaryName: Biosample.summaryName, + }, DNaseStart, DNaseEnd, H3K4me3Start, @@ -92,7 +97,7 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa dELS, pELS, PLS, - TF + TF, } const router = useRouter() @@ -102,7 +107,14 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa */ const biosampleTables = useMemo( () => { - const filteredBiosamples: FilteredBiosampleData = filterBiosamples(parseByCellType(props.byCellType), Tissue, PrimaryCell, CellLine, InVitro, Organoid) + const filteredBiosamples: FilteredBiosampleData = filterBiosamples( + parseByCellType(props.byCellType), + Tissue, + PrimaryCell, + CellLine, + InVitro, + Organoid + ) const cols = [ { header: "Biosample", @@ -203,7 +215,12 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa 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 }) + constructURL(props.mainQueryParams, urlParams, { + selected: true, + biosample: row.queryValue, + tissue: row.biosampleTissue, + summaryName: row.summaryName, + }) ) }} /> @@ -212,8 +229,7 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa ) } }) - } - , + }, // 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] @@ -238,7 +254,12 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa Tissue/Organ - setSearchString(event.target.value)} /> + setSearchString(event.target.value)} + /> {Biosample.selected && ( @@ -255,7 +276,14 @@ export default function MainResultsFilters(props: { mainQueryParams: MainQueryPa onClick={() => { setBiosample({ selected: false, biosample: null, tissue: null, summaryName: null }) setBiosampleHighlight(null) - router.push(constructURL(props.mainQueryParams, urlParams, { selected: false, biosample: null, tissue: null, summaryName: null })) + router.push( + constructURL(props.mainQueryParams, urlParams, { + selected: false, + biosample: null, + tissue: null, + summaryName: null, + }) + ) }} > Clear diff --git a/screen2.0/src/common/components/MainResultsTable.tsx b/screen2.0/src/common/components/MainResultsTable.tsx index 781c9e7c..2a90a098 100644 --- a/screen2.0/src/common/components/MainResultsTable.tsx +++ b/screen2.0/src/common/components/MainResultsTable.tsx @@ -3,9 +3,25 @@ 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 { Box, Button, Typography } from "@mui/material" +import Link from "next/link" let COLUMNS = (rows) => { + // can prob just use link instead here + const router = useRouter() + const pathname = usePathname() + const searchParams: any = useSearchParams()! + + const createQueryString = React.useCallback( + (name: string, value: string) => { + const params = new URLSearchParams(searchParams) + params.set(name, value) + + return params.toString() + }, + [searchParams] + ) + let col: DataTableColumn[] = [ { header: "Accession", @@ -63,14 +79,37 @@ let COLUMNS = (rows) => { header: "Linked\u00A0Genes\u00A0(Distance)", value: (row) => "", render: (row) => ( - <> - - {`PC:\u00A0${row.linkedGenes.pc[0].name},\u00A0${row.linkedGenes.pc[1].name},\u00A0${row.linkedGenes.pc[2].name}`} + + + {`PC: `} + + + {/* link to new tab - should use Link but won't nav after click without */} + + {` ${row.linkedGenes.pc[0].name}, `} + + {/* with button for onClick */} + + { + router.push(pathname.split("/")[0] + "?" + createQueryString("gene", row.linkedGenes.pc[0].name)) + }} + >{`${row.linkedGenes.pc[1].name}, `} + + {/* no button or link */} + {`${row.linkedGenes.pc[2].name}`} + + + + {`All: `} - - {`All:\u00A0${row.linkedGenes.all[0].name},\u00A0${row.linkedGenes.all[1].name},\u00A0${row.linkedGenes.all[2].name}`} + + {` ${row.linkedGenes.all[0].name}, `} + {`${row.linkedGenes.all[1].name}, `} + {`${row.linkedGenes.all[2].name}`} - > + ), }) diff --git a/screen2.0/src/common/components/ResponsiveAppBar.tsx b/screen2.0/src/common/components/ResponsiveAppBar.tsx index b06fc1e8..d655b1e6 100644 --- a/screen2.0/src/common/components/ResponsiveAppBar.tsx +++ b/screen2.0/src/common/components/ResponsiveAppBar.tsx @@ -5,6 +5,34 @@ import { AppBar, Box, Toolbar, IconButton, Typography, Menu, Container, Button, import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown" import MenuIcon from "@mui/icons-material/Menu" +import Grid2 from "@mui/material/Unstable_Grid2/Grid2" +import { + Autocomplete, + TextField, + Accordion, + AccordionDetails, + AccordionSummary, + debounce, + ButtonBase, + Checkbox, + FormControlLabel, + FormGroup, + ToggleButton, + ToggleButtonGroup, + Drawer, +} from "@mui/material" +import CloseIcon from "@mui/icons-material/Close" +import Divider from "@mui/material/Divider" +import ListItemButton from "@mui/material/ListItemButton" +import ListItemIcon from "@mui/material/ListItemIcon" +import ListItemText from "@mui/material/ListItemText" +import FolderIcon from "@mui/icons-material/Folder" +import ImageIcon from "@mui/icons-material/Image" +import DescriptionIcon from "@mui/icons-material/Description" +import InputBase from "@mui/material/InputBase" +import SearchIcon from "@mui/icons-material/Search" +import { ThemeProvider, createTheme } from "@mui/material/styles" + import Link from "next/link" import Image from "next/image" @@ -12,6 +40,7 @@ import HeaderSearch from "./HeaderSearch" import nextConfig from "../../../next.config" import screenIcon from "../../../public/screenIcon.png" +import { defaultTheme } from "../lib/themes" // CLICKING ON LINKS ONCE THE POPUP IS OPEN IS BROKEN!!! @@ -39,6 +68,7 @@ const pageLinks = [ dropdownID: "1", subPages: [ { pageName: "GWAS", link: "/applets/gwas" }, + { pageName: "Gene Expression", link: "/applets/gene-expression " }, { pageName: "Differential Gene Expression", link: "/applets/differential-gene-expression" }, { pageName: "Multi-Region Search", link: "/applets/multi-region-search" }, ], @@ -54,6 +84,7 @@ const pageLinks = [ */ function ResponsiveAppBar() { + const [open, setState] = React.useState(false) // Hamburger Menu, deals with setting it's position const [anchorElNav_Hamburger, setAnchorElNav_Hamburger] = React.useState(null) @@ -89,159 +120,196 @@ function ResponsiveAppBar() { } } + const toggleDrawer = (open) => (event) => { + if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) { + return + } + setState(open) + } + return ( - - - - {/* Display Icon on left when >=900px */} - - - - - SCREEN - - {/* Display Menu icon on left (and hide above icon and title) when <900px */} - - {/* Hamburger Menu, open on click */} - - - - + + + + {/* Display Icon on left when >=900px */} + + + + + SCREEN + + {/* Display Menu icon on left (and hide above icon and title) when <900px */} + + {/* Hamburger Menu, open on click */} + + + + + + + Home + + + {pageLinks.map((page) => ( + + {/* Wrap in next/link to enable dyanic link changing from basePath in next.config.js */} + + {page.pageName} + + + ))} + + + - - - Home - - + SCREEN + + {/* Main navigation items for desktop */} + {pageLinks.map((page) => ( - - {/* Wrap in next/link to enable dyanic link changing from basePath in next.config.js */} - - {page.pageName} - - - ))} - - - - SCREEN - - {/* Main navigation items for desktop */} - - {pageLinks.map((page) => ( - - } - onMouseEnter={page.subPages ? (event) => handleOpenNavMenu_Dropdown(event, page.dropdownID) : undefined} - > - {/* Wrap in next/link to enable dyanic link changing from basePath in next.config.js */} - {page.pageName} - - {/* Hover dropdowns, open on hover. Create new instance for each menu item */} - {page.subPages && ( - handleCloseNavMenu_Dropdown(page.dropdownID)} - //These are to prevent focus ring from showing up in some browsers, but doesn't work completely - MenuListProps={{ autoFocusItem: false, autoFocus: false }} + + } + onMouseEnter={page.subPages ? (event) => handleOpenNavMenu_Dropdown(event, page.dropdownID) : undefined} > - {page.subPages && - page.subPages.map((subPage) => ( - handleCloseNavMenu_Dropdown(page.dropdownID)}> - {/* Wrap in next/link to enable dyanic link changing from basePath in next.config.js */} - - {subPage.pageName} - - - ))} - - )} + {/* Wrap in next/link to enable dyanic link changing from basePath in next.config.js */} + {page.pageName} + + {/* Hover dropdowns, open on hover. Create new instance for each menu item */} + {page.subPages && ( + handleCloseNavMenu_Dropdown(page.dropdownID)} + //These are to prevent focus ring from showing up in some browsers, but doesn't work completely + MenuListProps={{ autoFocusItem: false, autoFocus: false }} + sx={{ + display: { xs: "block" }, + }} + > + {page.subPages && + page.subPages.map((subPage) => ( + handleCloseNavMenu_Dropdown(page.dropdownID)}> + {/* Wrap in next/link to enable dyanic link changing from basePath in next.config.js */} + + {subPage.pageName} + + + ))} + + )} + + ))} + + {/* TODO onSubmit for search box */} + + + + {/* Settings */} + + + + + + + + + + + + + Settings + - ))} - - {/* TODO onSubmit for search box */} - - - - - - + + + + + ) } export default ResponsiveAppBar diff --git a/screen2.0/src/common/lib/colors.ts b/screen2.0/src/common/lib/colors.ts index 2bfd9f2c..940ace8a 100644 --- a/screen2.0/src/common/lib/colors.ts +++ b/screen2.0/src/common/lib/colors.ts @@ -1,5 +1,60 @@ -export const geneRed = "#FF0000" -export const geneBlue = "#1E90FF" +/** + * cCRE Groups + */ +export const PLS = "#FFCD00" +export const pELS = "#FFA700" +export const dELS = "#FFCD00" +export const CAH3K4me3 = "#ffaaaa" +export const CA_CTCF = "#00B0F0" +export const CA_only = "#06DA93" +export const CA_TF = "#be28e5" +export const TF_only = "#d876ec" +export const LowDNase = "#e1e1e1" +export const Unclassified = "#8c8c8c" export const promoterRed = "red" export const enhancerYellow = "#E9D31C" + +// strand +export const geneRed = "#FF0000" +export const geneBlue = "#1E90FF" + +/** + * ENCODE Data + */ +export const DNase_seq = "#06DA93" +export const ATAC_seq = "#02c7b9" + +export const totalRNA_seq = "#00aa00" +export const longreadRNA_seq = "#006600" + +// Histone mark +export const H3K4me1 = "#FFDF00" +export const H3K4me3 = "#FF0000" +export const H3K27ac = "#FFCD00" +export const H3K36me3 = "#008000" +export const H3K9me3 = "#B4DDE4" +export const H3K27me3 = "#AEAFAE" + +// ChIA_PET +export const ChIA_PETRNAPII = "#b20600" +export const ChIA_PETCTCF = "#1900a6" +export const Hi_C = "#9222b0" + +// Bru_seq +export const Bru_seq = "#D642CA" +export const BruUV_seq = "" +export const BruChase_seq = "" + +// Pro +export const PRO_cap = "#f68800" +export const PRO_seq = "#b45f06" + +// other +export const RAMPAGEPeaksSignal = "#D642CA" +export const Conservation = "#999999" +export const Genes = "#000000" +export const TFChIP_seq = "#1262EB" +export const RBP = "#f68800" + +export const Enhancer_GeneLinks = "#A872E5" diff --git a/screen2.0/src/common/lib/filter-helpers.ts b/screen2.0/src/common/lib/filter-helpers.ts index 63b633c6..cfed7110 100644 --- a/screen2.0/src/common/lib/filter-helpers.ts +++ b/screen2.0/src/common/lib/filter-helpers.ts @@ -1,10 +1,4 @@ -import { - cCREData, - MainQueryParams, - CellTypeData, - UnfilteredBiosampleData, - FilteredBiosampleData -} from "../../app/search/types" +import { cCREData, MainQueryParams, CellTypeData, UnfilteredBiosampleData, FilteredBiosampleData } from "../../app/search/types" /** * @@ -104,9 +98,9 @@ function passesClassificationFilter(currentElement: cCREData, mainQueryParams: M } /** - * @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 - */ + * @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 @@ -122,10 +116,10 @@ function availableAssays( } /** - * - * @param byCellType JSON of byCellType - * @returns an object of sorted biosample types, grouped by tissue type - */ + * + * @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) => { @@ -164,7 +158,14 @@ export function parseByCellType(byCellType: CellTypeData): UnfilteredBiosampleDa * @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 { +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) => { @@ -196,8 +197,9 @@ export function assayHoverInfo(assays: { dnase: boolean; h3k27ac: boolean; h3k4m } 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" : "" - }` + 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 @@ -209,33 +211,33 @@ export function assayHoverInfo(assays: { dnase: boolean; h3k27ac: boolean; h3k4m 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, + 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; + selected: boolean + biosample: string + tissue: string summaryName: string } ) { @@ -243,24 +245,27 @@ export function constructURL( 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 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)}` + )}&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/src/common/lib/themes.ts b/screen2.0/src/common/lib/themes.ts new file mode 100644 index 00000000..90d8fa18 --- /dev/null +++ b/screen2.0/src/common/lib/themes.ts @@ -0,0 +1,48 @@ +import { ThemeProvider, createTheme } from "@mui/material/styles" + +// temp theme for toolbar color and accordion outline - UMass blue / empty secondary +export const defaultTheme = createTheme({ + palette: { + mode: "light", + primary: { + main: "#000F9F", + light: "#42a5f5", + dark: "#000F9F", + // contrastText: "#fff" + }, + secondary: { + main: "#nnn", + light: "#nnn", + dark: "#nnn", + // contrastText: "#fff" + }, + // background: { + // paper: "#fff", + // default: "#fff" + // } + }, + components: { + MuiAccordion: { + defaultProps: { + elevation: 0, // outline + }, + }, + }, + transitions: { + easing: { + easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)", + easeOut: "cubic-bezier(0.0, 0, 0.2, 1)", + easeIn: "cubic-bezier(0.4, 0, 1, 1)", + sharp: "cubic-bezier(0.4, 0, 0.6, 1)", + }, + duration: { + shortest: 150, + shorter: 200, + short: 250, + standard: 300, + complex: 375, + enteringScreen: 225, + leavingScreen: 195, + }, + }, +}) diff --git a/screen2.0/src/common/lib/utility.tsx b/screen2.0/src/common/lib/utility.tsx index 9238dad7..9f95eb4c 100644 --- a/screen2.0/src/common/lib/utility.tsx +++ b/screen2.0/src/common/lib/utility.tsx @@ -22,7 +22,7 @@ export async function fetchServer(url: string, jq: BodyInit) { .then((response) => { if (!response.ok) { // throw new Error(response.statusText) - return ErrorMessage(Error(response.statusText)) + return } return response.json() }) @@ -32,7 +32,7 @@ export async function fetchServer(url: string, jq: BodyInit) { .catch((error: Error) => { // logging // throw error - return ErrorMessage(error) + return }) } @@ -74,41 +74,40 @@ export function LoadingMessage() { * @param {Error} error * @returns error message */ -export function ErrorMessage(error: Error) { - let open: boolean = true - +export function ErrorMessage(props: { error: Error }) { // debugging // console.log("Error!") - console.log(error.message) + // console.log(props.error.message) + console.log(props.error) // throw error function toggleOpen(toggle: boolean) { - if (toggle === true) open = false - else open = true + // if (toggle === true) setOpen(false) + // else setOpen(true) } const handleClose = (event?: React.SyntheticEvent | Event, reason?: string) => { if (reason === "clickaway") { return } - open = false + // setOpen(false) } return ( - {/* */} - - Error - There was an error loading. — {error.message} - - {/* */} + + + Error + There was an error loading. — {"Error"} + + ) } diff --git a/screen2.0/types/types.ts b/screen2.0/types/types.ts index 9a2930b4..af1a454d 100644 --- a/screen2.0/types/types.ts +++ b/screen2.0/types/types.ts @@ -1,3 +1,5 @@ +export type QueryResponse = [number, string[], any, [string, string, string, string, string, string][], string[]] + export type GenomicRange = { chromosome: string; start: number; From 8545a514e73c1f8093a0d53dd4bf9ba31cd3fad9 Mon Sep 17 00:00:00 2001 From: Jordan <110398589+jordanvelezbomba@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:10:59 -0400 Subject: [PATCH 2/2] fixed collapse --- screen2.0/src/app/applets/gene-expression/utils.tsx | 4 ++-- screen2.0/src/app/search/ccredetails/gene-expression.tsx | 2 +- screen2.0/src/app/search/ccredetails/utils.tsx | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/screen2.0/src/app/applets/gene-expression/utils.tsx b/screen2.0/src/app/applets/gene-expression/utils.tsx index 54bb6636..cf7cef2a 100644 --- a/screen2.0/src/app/applets/gene-expression/utils.tsx +++ b/screen2.0/src/app/applets/gene-expression/utils.tsx @@ -115,7 +115,7 @@ export function PlotGeneExpression(props: { return ( @@ -129,7 +129,7 @@ export function PlotGeneExpression(props: { if (collapse[entry[0]] === undefined || collapse[entry[0]]) tmp[entry[0]] = false else tmp[entry[0]] = true } else { - tmp[x[0]] = (collapse[x[0]] !== undefined ? collapse[x[0]] : true) + tmp[x[0]] = collapse[x[0]] !== undefined ? collapse[x[0]] : true } }) setCollapse(tmp) diff --git a/screen2.0/src/app/search/ccredetails/gene-expression.tsx b/screen2.0/src/app/search/ccredetails/gene-expression.tsx index 8a500e98..ba5d1aa4 100644 --- a/screen2.0/src/app/search/ccredetails/gene-expression.tsx +++ b/screen2.0/src/app/search/ccredetails/gene-expression.tsx @@ -185,7 +185,7 @@ export function GeneExpression(props: { edge="start" color="inherit" aria-label="open drawer" - onClick={() => toggleDrawer(false)} + onClick={toggleDrawer(true)} sx={{ ...(open && { display: "none" }), }} diff --git a/screen2.0/src/app/search/ccredetails/utils.tsx b/screen2.0/src/app/search/ccredetails/utils.tsx index 5b854556..e42dfcab 100644 --- a/screen2.0/src/app/search/ccredetails/utils.tsx +++ b/screen2.0/src/app/search/ccredetails/utils.tsx @@ -176,9 +176,9 @@ export function PlotActivityProfiles(props: { data: any; range: Range2D; dimensi return ( } @@ -190,7 +190,7 @@ export function PlotActivityProfiles(props: { data: any; range: Range2D; dimensi if (collapse[entry[0]] === undefined || collapse[entry[0]]) tmp[entry[0]] = false else tmp[entry[0]] = true } else { - tmp[x[0]] = (collapse[x[0]] !== undefined ? collapse[x[0]] : true) + tmp[x[0]] = collapse[x[0]] !== undefined ? collapse[x[0]] : true } }) setCollapse(tmp)