From 9e37586679a08f158a1d1e83b57a70ce987c0ab6 Mon Sep 17 00:00:00 2001 From: Christopher Andrew Mancuso Date: Thu, 19 Sep 2024 12:45:42 -0400 Subject: [PATCH] Changes from PyGenePlexus (#45) The items in here that changed are from commits 5722d9b, d07ce39 and 62cfc9a in the PyGenePlexus repo. Things that specifically changed that might need a UI tweak here - In both the probability and similarity tables there are columns from z-scores and p-values - The edge lists are now weighted again - using Mondo instead of DisGeNet for disease-gene annotations - In similarity table there is now a column called "Task" that describes the type of GSC it is from - changed how the auPRC values are returned, especially when they can't be calculated - Added a column "Gene Name" to the tables the show how the input IDs are converted - If training and showing results for different species, the probability data frame now can have known-novel and Class-Label columns and this information is based on one-to-one ortholog info. Can be used in network figure. Other UI ask that could be good in this PR is possibly showing only StTRING as an option and then having a button a user clicks labeled "More Options" that would show the BioGRID and IMP networks. Similarity the GSC could be default to Combined and the user needs to click a button to see the individual GSCs. This is #44 I also added a script to gather and zip the data for the function. I uploaded the new data to the GCP bucket. --------- Co-authored-by: Vincent Rubinetti --- .gitignore | 2 + frontend/src/api/types.ts | 50 ++++--- frontend/src/components/Heading.module.css | 2 +- frontend/src/components/Radios.tsx | 15 +- frontend/src/components/Table.tsx | 30 +++- frontend/src/global/layout.css | 5 + frontend/src/global/styles.css | 4 +- frontend/src/global/theme.css | 8 +- frontend/src/pages/NewAnalysis.tsx | 26 +--- frontend/src/pages/Testbed.tsx | 40 ++--- frontend/src/pages/analysis/InputGenes.tsx | 4 + frontend/src/pages/analysis/Network.tsx | 139 ++++++++++-------- frontend/src/pages/analysis/Predictions.tsx | 28 ++-- frontend/src/pages/analysis/Similarities.tsx | 27 +++- frontend/src/pages/analysis/Summary.tsx | 4 +- frontend/src/util/dom.ts | 23 +++ frontend/src/util/string.ts | 2 +- functions/README.md | 40 +++-- .../convert_ids_deploy/requirements.txt | 2 +- functions/convert_ids/gather_data_ids.sh | 19 +++ functions/ml/gather_data_ml.sh | 18 +++ functions/ml/ml_deploy/requirements.txt | 4 +- 22 files changed, 322 insertions(+), 170 deletions(-) create mode 100755 functions/convert_ids/gather_data_ids.sh create mode 100755 functions/ml/gather_data_ml.sh diff --git a/.gitignore b/.gitignore index cfaa85f..8809fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ __pycache__/ /.keys/* data +functions/convert_ids/convert-ids_data.tar.gz +functions/ml/ml_data.tar.gz diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 9c27b77..732a168 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -16,23 +16,24 @@ export type Species = export type Network = "BioGRID" | "STRING" | "IMP"; /** geneset context options */ -export type GenesetContext = "GO" | "Monarch" | "DisGeNet" | "Combined"; +export type GenesetContext = "GO" | "Monarch" | "Mondo" | "Combined"; /** convert-ids endpoint response format */ export type _ConvertIds = { input_count: number; convert_ids: string[]; table_summary: { - Network: string; + Network: Network; NetworkGenes: number; PositiveGenes: number; }[]; df_convert_out: { + "Original ID": string; "Entrez ID": string; + "Gene Name": string; "In BioGRID?": string; "In IMP?": string; "In STRING?": string; - "Original ID": string; }[]; }; @@ -55,9 +56,9 @@ export const convertConvertIds = (backend: _ConvertIds) => { table: backend.df_convert_out.map((row) => ({ input: row["Original ID"], entrez: row["Entrez ID"], - biogrid: row["In BioGRID?"] === "Y", - imp: row["In IMP?"] === "Y", - string: row["In STRING?"] === "Y", + name: row["Gene Name"], + inNetwork: + (row["In BioGRID?"] ?? row["In IMP?"] ?? row["In STRING?"]) === "Y", })), }; }; @@ -100,32 +101,38 @@ export const revertAnalysisInputs = ( /** convert-ids endpoint response format */ export type _AnalysisResults = { df_convert_out_subset: { + "Original ID": string; "Entrez ID": string; + "Gene Name": string; "In BioGRID?"?: string; "In IMP?"?: string; "In STRING?"?: string; - "Original ID": string; }[]; - avgps: number[]; + avgps: (number | null | undefined)[]; positive_genes: number; isolated_genes: string[]; isolated_genes_sym: string[]; - df_edge: { Node1: string; Node2: string }[]; - df_edge_sym: { Node1: string; Node2: string }[]; + df_edge: { Node1: string; Node2: string; Weight: number }[]; + df_edge_sym: { Node1: string; Node2: string; Weight: number }[]; df_probs: { Rank: number; Entrez: string; Symbol: string; Name: string; - Probability: number; "Known/Novel": "Known" | "Novel"; "Class-Label": "P" | "N" | "U"; + Probability: number; + "Z-score": number; + "P-adjusted": number; }[]; df_sim: { Rank: number; + Task: string; ID: string; Name: string; Similarity: number; + "Z-score": number; + "P-adjusted": number; }[]; }; @@ -136,39 +143,48 @@ export const convertAnalysisResults = (backend: _AnalysisResults) => ({ entrez: row["Entrez ID"].match(/Could Not be mapped to Entrez/i) ? "" : row["Entrez ID"], + name: row["Gene Name"], inNetwork: (row["In BioGRID?"] ?? row["In IMP?"] ?? row["In STRING?"]) === "Y", })), crossValidation: backend.avgps, positiveGenes: backend.positive_genes, predictions: backend.df_probs.map((row) => ({ - rank: row.Rank, entrez: row.Entrez, symbol: row.Symbol, name: row.Name, - probability: row.Probability, knownNovel: row["Known/Novel"], classLabel: expandClass(row["Class-Label"]), + probability: row.Probability, + zScore: row["Z-score"], + pAdjusted: row["P-adjusted"], + rank: row.Rank, })), similarities: backend.df_sim.map((row) => ({ - rank: row.Rank, + task: row.Task, id: row.ID, name: row.Name, similarity: row.Similarity, + zScore: row["Z-score"], + pAdjusted: row["P-adjusted"], + rank: row.Rank, })), network: { nodes: backend.df_probs.map((row) => ({ - rank: row.Rank, - probability: row.Probability, entrez: row.Entrez, symbol: row.Symbol, name: row.Name, knownNovel: row["Known/Novel"], classLabel: expandClass(row["Class-Label"]), + probability: row.Probability, + zScore: row["Z-score"], + pAdjusted: row["P-adjusted"], + rank: row.Rank, })), - links: backend.df_edge.map((row) => ({ + edges: backend.df_edge.map((row) => ({ source: row.Node1, target: row.Node2, + weight: row.Weight, })), }, }); diff --git a/frontend/src/components/Heading.module.css b/frontend/src/components/Heading.module.css index 04680c3..4735e11 100644 --- a/frontend/src/components/Heading.module.css +++ b/frontend/src/components/Heading.module.css @@ -19,7 +19,7 @@ h4.heading { } .icon { - color: var(--deep-mid); + color: var(--deep); } .anchor { diff --git a/frontend/src/components/Radios.tsx b/frontend/src/components/Radios.tsx index 8ec7272..32d2fa8 100644 --- a/frontend/src/components/Radios.tsx +++ b/frontend/src/components/Radios.tsx @@ -1,6 +1,6 @@ import type { ReactElement, ReactNode } from "react"; import { cloneElement, useEffect, useId, useState } from "react"; -import { FaRegCircle, FaRegCircleDot } from "react-icons/fa6"; +import { FaCircleDot, FaRegCircle } from "react-icons/fa6"; import clsx from "clsx"; import { usePrevious } from "@reactuses/core"; import Flex from "@/components/Flex"; @@ -110,16 +110,21 @@ const Radios = ({ {/* check mark */} {selectedWFallback === option.id ? ( - + ) : ( )} {/* text content */} - {option.primary} + + {option.primary} + {option.secondary && ( {option.secondary} )} diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx index d1fc8be..557e93f 100644 --- a/frontend/src/components/Table.tsx +++ b/frontend/src/components/Table.tsx @@ -41,7 +41,7 @@ import Slider from "@/components/Slider"; import TextBox from "@/components/TextBox"; import Tooltip from "@/components/Tooltip"; import { downloadCsv } from "@/util/download"; -import { formatNumber } from "@/util/string"; +import { formatDate, formatNumber } from "@/util/string"; import classes from "./Table.module.css"; type Col< @@ -69,7 +69,10 @@ type Col< show?: boolean; /** tooltip to show in header cell */ tooltip?: ReactNode; - /** custom render function for cell */ + /** + * custom render function for cell. return undefined or null to fallback to + * default formatting. + */ render?: (cell: NoInfer) => ReactNode; }; @@ -212,8 +215,13 @@ const Table = ({ cols, rows }: Props) => { /** func to use for filtering individual column */ filterFn: filterFunc, /** render func for cell */ - cell: (cell) => - col.render ? col.render(cell.getValue()) : cell.getValue(), + cell: (cell) => { + const raw = cell.getValue(); + const rendered = col.render?.(raw); + return rendered === undefined || rendered === null + ? defaultFormat(raw) + : rendered; + }, }), ); @@ -633,3 +641,17 @@ const Filter = ({ column, def }: FilterProps) => { /> ); }; + +/** default cell formatter based on detected type */ +const defaultFormat = (cell: unknown) => { + if (typeof cell === "number") return formatNumber(cell); + if (typeof cell === "boolean") return cell ? "True" : "False"; + /** if falsey (except 0 and false) */ + if (!cell) return "-"; + if (Array.isArray(cell)) return cell.length.toLocaleString(); + if (cell instanceof Date) return formatDate(cell); + if (typeof cell === "object") + return Object.keys(cell).length.toLocaleString(); + if (typeof cell === "string") return cell; + return String(cell); +}; diff --git a/frontend/src/global/layout.css b/frontend/src/global/layout.css index 1573734..34fe547 100644 --- a/frontend/src/global/layout.css +++ b/frontend/src/global/layout.css @@ -65,6 +65,11 @@ font-weight: var(--medium); } +.mini-table > :nth-child(even):empty::after { + content: "-"; + color: var(--gray); +} + /** automatically applied to all icons (react-icons and imported custom icons) */ .icon { diff --git a/frontend/src/global/styles.css b/frontend/src/global/styles.css index c629c22..8ddd324 100644 --- a/frontend/src/global/styles.css +++ b/frontend/src/global/styles.css @@ -78,7 +78,7 @@ hr { /** text */ a { - color: var(--accent); + color: var(--deep); transition: color var(--fast); } @@ -87,7 +87,7 @@ a:hover { } p a:visited { - color: var(--deep); + color: var(--accent); } p a:visited:hover { diff --git a/frontend/src/global/theme.css b/frontend/src/global/theme.css index 078fff3..bc15f1c 100644 --- a/frontend/src/global/theme.css +++ b/frontend/src/global/theme.css @@ -1,11 +1,11 @@ :root { /* accent */ - --accent: #ba3960; + --accent: hsl(342, 50%, 50%); + --accent-light: hsl(342, 100%, 90%); /** deep */ - --deep: #22577a; - --deep-mid: #5a92b7; - --deep-light: #ceebff; + --deep: hsl(204, 50%, 35%); + --deep-light: hsl(204, 100%, 90%); /** grays */ --black: #000000; diff --git a/frontend/src/pages/NewAnalysis.tsx b/frontend/src/pages/NewAnalysis.tsx index 442e88b..90c04a8 100644 --- a/frontend/src/pages/NewAnalysis.tsx +++ b/frontend/src/pages/NewAnalysis.tsx @@ -98,8 +98,8 @@ const genesetContextOptions: RadioOption[] = [ secondary: "Phenotypes", }, { - id: "DisGeNet", - primary: "DisGeNet", + id: "Mondo", + primary: "Mondo", secondary: "Diseases", }, ]; @@ -184,7 +184,7 @@ const NewAnalysisPage = () => { /** restrict species options based on other params */ const filteredSpeciesOptions = speciesOptions.filter((option) => { - if (genesetContext === "DisGeNet" && option.id !== "Human") return false; + if (genesetContext === "Mondo" && option.id !== "Human") return false; if (network === "BioGRID" && option.id === "Zebrafish") return false; return true; }); @@ -196,10 +196,10 @@ const NewAnalysisPage = () => { ) toast("BioGRID does not support Zebrafish.", "warning", "warn1"); if ( - genesetContext === "DisGeNet" && + genesetContext === "Mondo" && (speciesTrain !== "Human" || speciesTest !== "Human") ) - toast("DisGeNet only supports Human genes.", "warning", "warn2"); + toast("Mondo only supports Human genes.", "warning", "warn2"); /** auto-select species */ useEffect(() => { @@ -386,20 +386,8 @@ const NewAnalysisPage = () => { render: (cell) => cell || Failed, }, { - key: "biogrid", - name: "In BioGRID", - render: YesNo, - filterType: "boolean", - }, - { - key: "imp", - name: "In IMP", - render: YesNo, - filterType: "boolean", - }, - { - key: "string", - name: "In STRING", + key: "inNetwork", + name: "In Network", render: YesNo, filterType: "boolean", }, diff --git a/frontend/src/pages/Testbed.tsx b/frontend/src/pages/Testbed.tsx index 64e238e..bbd9482 100644 --- a/frontend/src/pages/Testbed.tsx +++ b/frontend/src/pages/Testbed.tsx @@ -50,6 +50,7 @@ import TextBox from "@/components/TextBox"; import Tile from "@/components/Tile"; import { toast } from "@/components/Toasts"; import Tooltip from "@/components/Tooltip"; +import { themeVariables } from "@/util/dom"; import { formatDate, formatNumber } from "@/util/string"; import tableData from "../../fixtures/table.json"; @@ -78,33 +79,18 @@ const TestbedPage = () => { {/* color palette */} - {[ - "accent", - "deep", - "deep-mid", - "deep-light", - "black", - "off-black", - "dark-gray", - "gray", - "light-gray", - "off-white", - "white", - "success", - "warning", - "error", - ].map((color, index) => ( - -
- - ))} + {Object.entries(themeVariables) + .filter( + ([, value]) => value.startsWith("#") || value.startsWith("hsl"), + ) + .map(([variable, color], index) => ( + +
+ + ))}

diff --git a/frontend/src/pages/analysis/InputGenes.tsx b/frontend/src/pages/analysis/InputGenes.tsx index 1b83f90..77c631e 100644 --- a/frontend/src/pages/analysis/InputGenes.tsx +++ b/frontend/src/pages/analysis/InputGenes.tsx @@ -19,6 +19,10 @@ const InputGenes = ({ results }: Props) => { name: "Entrez ID", render: (cell) => cell || Failed, }, + { + key: "name", + name: "Name", + }, { key: "inNetwork", name: "In Network", diff --git a/frontend/src/pages/analysis/Network.tsx b/frontend/src/pages/analysis/Network.tsx index 9435668..c87ea4c 100644 --- a/frontend/src/pages/analysis/Network.tsx +++ b/frontend/src/pages/analysis/Network.tsx @@ -24,6 +24,7 @@ import Flex from "@/components/Flex"; import type { Option } from "@/components/SelectSingle"; import SelectSingle from "@/components/SelectSingle"; import Slider from "@/components/Slider"; +import { theme } from "@/util/dom"; import { downloadSvg } from "@/util/download"; import { lerp } from "@/util/math"; import { formatNumber } from "@/util/string"; @@ -49,16 +50,16 @@ const attractionStrength = 1; const springDistance = 40; /** node circle fill colors (keep light to allow dark text) */ const nodeColors: Record = { - Positive: "#b3e2ff", - Negative: "#ffcada", - Neutral: "#e8e8e8", + Positive: theme("--deep-light"), + Negative: theme("--accent-light"), + Neutral: theme("--light-gray"), /** fallback */ - Node: "#e8e8e8", + Node: theme("--light-gray"), }; -/** link line stroke color */ -const linkColor = "#a0a0a0"; -/** selected link line stroke color */ -const selectedLinkColor = "#ba3960"; +/** edge line stroke color */ +const edgeColor = theme("--light-gray"); +/** selected edge line stroke color */ +const selectedEdgeColor = theme("--accent"); /** legend square/text/other size */ const legendCell = 15; /** legend line spacing factor */ @@ -88,7 +89,7 @@ const Network = ({ inputs, results }: Props) => { const svgRef = useRef(null); const zoomRef = useRef(null); const legendRef = useRef(null); - const linkRefs = useRef>(new Map()); + const edgeRefs = useRef>(new Map()); const circleRefs = useRef>(new Map()); const labelRefs = useRef>(new Map()); @@ -97,13 +98,21 @@ const Network = ({ inputs, results }: Props) => { Math.min(Math.floor(hardMaxNodes / 10), results.network.nodes.length), ); - /** min/max of probabilities */ - const probabilityExtent = d3.extent( + /** min/max of node probabilities */ + const [minNodeProb = 0, maxNodeProb = 1] = d3.extent( results.network.nodes.map((node) => node.probability), ); /** don't display nodes below this probability */ - const [minProbability, setMinProbability] = useState(probabilityExtent[0]!); + const [nodeProbThresh, setNodeProbThresh] = useState(0.0002); + + /** don't display edges below this weight */ + const [edgeWeightThresh, setEdgeWeightThresh] = useState(0); + + /** min/max of edge weights */ + const [minEdgeWeight = 0, maxEdgeWeight = 1] = d3.extent( + results.network.edges.map((edge) => edge.weight), + ); /** whether to auto-fit camera to contents every frame */ const [autoFit, setAutoFit] = useState(false); @@ -118,9 +127,9 @@ const Network = ({ inputs, results }: Props) => { const nodes = useMemo( () => results.network.nodes - .filter((node) => node.probability >= minProbability) + .filter((node) => node.probability >= nodeProbThresh) .slice(0, maxNodes), - [results.network.nodes, minProbability, maxNodes], + [results.network.nodes, nodeProbThresh, maxNodes], ); /** quick node lookup */ @@ -129,19 +138,22 @@ const Network = ({ inputs, results }: Props) => { [nodes], ); - /** links to display */ - const links = useMemo( + /** edges to display */ + const edges = useMemo( () => - results.network.links + results.network.edges .filter( - (link) => link.source in nodeLookup && link.target in nodeLookup, + (edge) => + edge.source in nodeLookup && + edge.target in nodeLookup && + edge.weight > edgeWeightThresh, ) - .map((link) => ({ - ...link, - sourceIndex: nodeLookup[link.source]!, - targetIndex: nodeLookup[link.target]!, + .map((edge) => ({ + ...edge, + sourceIndex: nodeLookup[edge.source]!, + targetIndex: nodeLookup[edge.target]!, })), - [nodeLookup, results.network.links], + [nodeLookup, results.network.edges, edgeWeightThresh], ); /** legend info */ @@ -160,7 +172,7 @@ const Network = ({ inputs, results }: Props) => { /** counts */ legend.push([ ["Nodes", formatNumber(nodes.length)], - ["Links", formatNumber(links.length)], + ["Edges", formatNumber(edges.length)], ]); /** selected node info */ @@ -254,13 +266,13 @@ const Network = ({ inputs, results }: Props) => { label?.setAttribute("y", String(node.y)); }); - /** position links */ + /** position edges */ (simulation.force("spring") as typeof spring) .links() - .forEach((link, index) => { - const line = linkRefs.current.get(index); - const source = simulation.nodes().at(link.sourceIndex); - const target = simulation.nodes().at(link.targetIndex); + .forEach((edge, index) => { + const line = edgeRefs.current.get(index); + const source = simulation.nodes().at(edge.sourceIndex); + const target = simulation.nodes().at(edge.targetIndex); if (source) { line?.setAttribute("x1", String(source.x)); line?.setAttribute("y1", String(source.y)); @@ -290,7 +302,7 @@ const Network = ({ inputs, results }: Props) => { node.fx = event.x; node.fy = event.y; /** reheat */ - simulation.alpha(1).restart(); + simulation.alpha(0.1).restart(); }) .on("end", (event, d) => { /** get node being dragged from datum index */ @@ -303,7 +315,7 @@ const Network = ({ inputs, results }: Props) => { [simulation], ); - /** update simulation to be in-sync with declarative nodes/links */ + /** update simulation to be in-sync with declarative nodes/edges */ useEffect(() => { /** update nodes */ const d3nodeLookup = Object.fromEntries( @@ -313,12 +325,12 @@ const Network = ({ inputs, results }: Props) => { nodes.map((node) => d3nodeLookup[node.entrez] ?? { id: node.entrez }), ); - /** update links */ - (simulation.force("spring") as typeof spring).links(cloneDeep(links)); + /** update edges */ + (simulation.force("spring") as typeof spring).links(cloneDeep(edges)); /** reheat */ - simulation.alpha(1).restart(); - }, [nodes, links, simulation]); + simulation.alpha(0.1).restart(); + }, [nodes, edges, simulation]); /** fit background rectangle to fit contents of legend */ const fitLegend = useCallback(() => { @@ -379,13 +391,22 @@ const Network = ({ inputs, results }: Props) => { onChange={setMaxNodes} /> + @@ -414,11 +435,11 @@ const Network = ({ inputs, results }: Props) => { > {/* zoom camera */} - {/* links */} + {/* edges */} { )} pointerEvents="none" > - {links.map((link, index) => { - /** is link connected to selected node */ + {edges.map((edge, index) => { + /** is edge connected to selected node */ const selected = selectedNode - ? link.source === selectedNode.entrez || - link.target === selectedNode.entrez + ? edge.source === selectedNode.entrez || + edge.target === selectedNode.entrez : undefined; return ( { - if (el) linkRefs.current.set(index, el); - else linkRefs.current.delete(index); + if (el) edgeRefs.current.set(index, el); + else edgeRefs.current.delete(index); }} key={index} stroke={ selected === true - ? selectedLinkColor + ? selectedEdgeColor : selected === false ? "transparent" : "" } - strokeWidth={selected === true ? nodeRadius / 10 : ""} + strokeWidth={edge.weight / 2} /> ); })} @@ -473,7 +494,9 @@ const Network = ({ inputs, results }: Props) => { nodeRadius / 2, )} fill={nodeColors[node.classLabel || "Node"]} - stroke={node.entrez === selectedNode?.entrez ? "#000000" : ""} + stroke={ + node.entrez === selectedNode?.entrez ? theme("--black") : "" + } tabIndex={0} onClick={() => setSelectedNode(node)} onKeyDown={(event) => { @@ -486,7 +509,7 @@ const Network = ({ inputs, results }: Props) => { {/* node labels */} { {/* legend */} {/* background */} - + {/* info */} {legend?.map((group) => { @@ -530,7 +553,7 @@ const Network = ({ inputs, results }: Props) => { legendLine++; /** is colored icon to show */ - const icon = key.startsWith("#"); + const icon = key.startsWith("#") || key.startsWith("hsl"); /** line y */ const y = legendLine * legendCell * legendSpacing; @@ -549,7 +572,7 @@ const Network = ({ inputs, results }: Props) => { y={y} fontSize={legendCell} dominantBaseline="central" - fill="#808080" + fill={theme("--dark-gray")} > {key} @@ -620,7 +643,7 @@ const attraction = d3 .strength(attractionStrength) .distanceMin(nodeRadius); -/** push/pull nodes together based on links */ +/** push/pull nodes together based on edges */ const spring = d3 .forceLink() .distance(springDistance) diff --git a/frontend/src/pages/analysis/Predictions.tsx b/frontend/src/pages/analysis/Predictions.tsx index 76fdcc1..768ddd5 100644 --- a/frontend/src/pages/analysis/Predictions.tsx +++ b/frontend/src/pages/analysis/Predictions.tsx @@ -1,7 +1,6 @@ import type { AnalysisResults } from "@/api/types"; import Exponential from "@/components/Exponential"; import Table from "@/components/Table"; -import { formatNumber } from "@/util/string"; type Props = { results: AnalysisResults; @@ -16,14 +15,6 @@ const Predictions = ({ results }: Props) => { name: "Rank", filterType: "number", }, - { - key: "probability", - name: "Probability", - tooltip: "Indicates gene's network-based similarity to input genes", - filterType: "number", - render: (cell) => - cell < 0.01 ? : formatNumber(cell), - }, { key: "entrez", name: "Entrez", @@ -53,6 +44,25 @@ const Predictions = ({ results }: Props) => { filterType: "enum", style: { whiteSpace: "nowrap" }, }, + { + key: "probability", + name: "Probability", + tooltip: "Indicates gene's network-based similarity to input genes", + filterType: "number", + render: (cell) => (cell < 0.01 ? : null), + }, + { + key: "zScore", + name: "z-score", + filterType: "number", + style: { whiteSpace: "nowrap" }, + }, + { + key: "pAdjusted", + name: "p-adjusted", + filterType: "number", + style: { whiteSpace: "nowrap" }, + }, ]} rows={results.predictions} /> diff --git a/frontend/src/pages/analysis/Similarities.tsx b/frontend/src/pages/analysis/Similarities.tsx index 784b5da..d589637 100644 --- a/frontend/src/pages/analysis/Similarities.tsx +++ b/frontend/src/pages/analysis/Similarities.tsx @@ -1,7 +1,7 @@ import type { AnalysisResults } from "@/api/types"; +import Exponential from "@/components/Exponential"; import Link from "@/components/Link"; import Table from "@/components/Table"; -import { formatNumber } from "@/util/string"; type Props = { results: AnalysisResults; @@ -15,9 +15,9 @@ const Similarities = ({ results }: Props) => { Gene Ontology Biological Process {" "} - terms or DisGeNet diseases - ranked by their similarity to the custom ML model trained on the input - genes. + terms or Mondo{" "} + diseases ranked by their similarity to the custom ML model trained on + the input genes.

{ name: "Rank", filterType: "number", }, + { + key: "task", + name: "Task", + }, { key: "id", name: "ID", @@ -47,7 +51,20 @@ const Similarities = ({ results }: Props) => { key: "similarity", name: "Similarity", filterType: "number", - render: formatNumber, + }, + { + key: "zScore", + name: "z-score", + filterType: "number", + style: { whiteSpace: "nowrap" }, + }, + { + key: "pAdjusted", + name: "p-adjusted", + filterType: "number", + style: { whiteSpace: "nowrap" }, + render: (cell) => + cell < 0.01 ? : null, }, ]} rows={results.similarities} diff --git a/frontend/src/pages/analysis/Summary.tsx b/frontend/src/pages/analysis/Summary.tsx index 41384a7..079be1c 100644 --- a/frontend/src/pages/analysis/Summary.tsx +++ b/frontend/src/pages/analysis/Summary.tsx @@ -36,7 +36,9 @@ const Summary = ({ results }: Props) => { {results.crossValidation - .map((value) => value.toFixed(2)) + .map((value) => + typeof value === "number" ? value.toFixed(2) : "-", + ) .join(", ")} diff --git a/frontend/src/util/dom.ts b/frontend/src/util/dom.ts index f8b0436..33732c4 100644 --- a/frontend/src/util/dom.ts +++ b/frontend/src/util/dom.ts @@ -3,6 +3,29 @@ import reactToText from "react-to-text"; import { debounce } from "lodash"; import { sleep } from "@/util/misc"; +/** css on :root */ +const rootStyles = window.getComputedStyle(document.documentElement); + +/** theme css variables https://stackoverflow.com/a/78994961/2180570 */ +export const themeVariables = Object.fromEntries( + Array.from(document.styleSheets) + .flatMap((styleSheet) => { + try { + return Array.from(styleSheet.cssRules); + } catch (error) { + return []; + } + }) + .filter((cssRule) => cssRule instanceof CSSStyleRule) + .flatMap((cssRule) => Array.from(cssRule.style)) + .filter((style) => style.startsWith("--")) + .map((variable) => [variable, rootStyles.getPropertyValue(variable)]), +); + +/** get css theme variable */ +export const theme = (variable: `--${string}`) => + themeVariables[variable] ?? ""; + /** wait for element matching selector to appear, checking periodically */ export const waitFor = async ( selector: string, diff --git a/frontend/src/util/string.ts b/frontend/src/util/string.ts index 590db38..27f9668 100644 --- a/frontend/src/util/string.ts +++ b/frontend/src/util/string.ts @@ -39,7 +39,7 @@ export const formatDate = (date: string | Date | undefined) => { dateStyle: "medium", timeStyle: "short", }); - return null; + return ""; }; /** make label (e.g. aria label) from html string */ diff --git a/functions/README.md b/functions/README.md index dbbc777..1a391bc 100644 --- a/functions/README.md +++ b/functions/README.md @@ -71,10 +71,12 @@ type Response = { }[]; // dataframe of results df_convert_out: { - // converted id of gene - "Entrez ID": string; // input id of gene "Original ID": string; + // converted id of gene + "Entrez ID": string; + // converted name of gene + "Gene Name": string; // whether gene was found in each network "In BioGRID?": "Y" | "N"; "In IMP?": "Y" | "N"; @@ -100,7 +102,7 @@ type Request = { // network that ML features are from and which edge list is used to make final graph net_type: "BioGRID" | "STRING" | "IMP"; // source used to select negative genes and which sets to compare trained model to - gsc: "GO" | "Monarch" | "DisGeNet" | "Combined"; + gsc: "GO" | "Monarch" | "Mondo" | "Combined"; }; ``` @@ -115,6 +117,7 @@ type Response = { df_convert_out_subset: { "Original ID": string; "Entrez ID": string; + "Gene Name": string; // only one of these present, based on selected network "In BioGRID?"?: string; "In IMP?"?: string; @@ -122,19 +125,18 @@ type Response = { }[]; // cross validation results, performance measured using log2(auprc/prior) - avgps: int[]; + avgps: (int | None)[]; // number of genes considered positives in network - positive_genes: number; + positive_genes: int; // top predicted genes that are isolated from other top predicted genes in network (as Entrez IDs) isolated_genes: string[]; // top predicted genes that are isolated from other top predicted genes in network (as gene symbols) isolated_genes_sym: string[]; - // edge list corresponding to subgraph induced by top predicted genes (as Entrez IDs) - df_edge: { Node1: string; Node2: string }[]; + df_edge: { Node1: string; Node2: string; Weight: number }[]; // edge list corresponding to subgraph induced by top predicted genes (as gene symbols) - df_edge_sym: { Node1: string; Node2: string }[]; + df_edge_sym: { Node1: string; Node2: string; Weight: number }[]; // table showing how associated each gene in prediction species network is to the users gene list df_probs: { @@ -146,24 +148,34 @@ type Response = { "Symbol": string; // full gene name "Name": string; - // probability of gene being part of input gene list - "Probability": number; // whether gene is in input gene list "Known/Novel": "Known" | "Novel"; // gene class, positive | negative | neutraul "Class-Label": "P" | "N" | "U"; + // probability of gene being part of input gene list + "Probability": number; + // z-score of the probabilities + "Z-score": number; + // adjusted p-values of the z-scores + "P-adjusted": number; }[]; // table showing how similar user's trained model is to models trained on known gene sets df_sim: { // rank of similarity between input model and a model trained on term gene set - Rank: int; + "Rank": int; + // type of term + "Task": string; // term ID - ID: string; + "ID": string; // term name - Name: string; + "Name": string; // similarity between input model and a model trained on term gene set - Similarity: number; + "Similarity": number; + // z-score of the similarities + "Z-score": number; + // adjusted p-values of the z-scores + "P-adjusted": number; }[]; }; ``` diff --git a/functions/convert_ids/convert_ids_deploy/requirements.txt b/functions/convert_ids/convert_ids_deploy/requirements.txt index acfdc21..e0703ce 100644 --- a/functions/convert_ids/convert_ids_deploy/requirements.txt +++ b/functions/convert_ids/convert_ids_deploy/requirements.txt @@ -1,4 +1,4 @@ # An example requirements file. If your function has other dependencies, # add them below functions-framework==3.* -geneplexus @ git+https://github.com/krishnanlab/PyGenePlexus@3499a60 \ No newline at end of file +geneplexus @ git+https://github.com/krishnanlab/PyGenePlexus@5722d9b \ No newline at end of file diff --git a/functions/convert_ids/gather_data_ids.sh b/functions/convert_ids/gather_data_ids.sh new file mode 100755 index 0000000..6b8c07b --- /dev/null +++ b/functions/convert_ids/gather_data_ids.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# this function will get data generated in GenePlexus_Backend +# and add it to function and zip it to be moved to the cloud + +# set dir to backend data +dir1=/Users/mancchri/Desktop/CIDA_unsorted/Arjun/GenePlexusZoo_webserver/gpdata_prop/regular + +# make dir and move in data +mkdir convert-ids_data +cp $dir1/IDconversion*.json convert-ids_data +cp $dir1/NodeOrder*.txt convert-ids_data +# tar this dir (note this compresseed file is manually uploaded to GCP later) +tar -czvf convert-ids_data.tar.gz convert-ids_data +# rename the dir to be move into gcp function +mv convert-ids_data data +# delete old data file and move new one in +rm -rf convert_ids_deploy/data +mv data convert_ids_deploy \ No newline at end of file diff --git a/functions/ml/gather_data_ml.sh b/functions/ml/gather_data_ml.sh new file mode 100755 index 0000000..7a20bf0 --- /dev/null +++ b/functions/ml/gather_data_ml.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# this function will get data generated in GenePlexus_Backend +# and add it to function and zip it to be moved to the cloud + +# set dir to backend data +dir1=/Users/mancchri/Desktop/CIDA_unsorted/Arjun/GenePlexusZoo_webserver/gpdata_prop/regular + +# make dir and move in data +mkdir ml_data +cp $dir1/* ml_data +# tar this dir (note this compresseed file is manually uploaded to GCP later) +tar -czvf ml_data.tar.gz ml_data +# rename the dir to be move into gcp function +mv ml_data data +# delete old data file and move new one in +rm -rf ml_deploy/data +mv data ml_deploy \ No newline at end of file diff --git a/functions/ml/ml_deploy/requirements.txt b/functions/ml/ml_deploy/requirements.txt index 990e666..adefe79 100644 --- a/functions/ml/ml_deploy/requirements.txt +++ b/functions/ml/ml_deploy/requirements.txt @@ -1,5 +1,5 @@ # An example requirements file. If your function has other dependencies, # add them below functions-framework==3.* -geneplexus @ git+https://github.com/krishnanlab/PyGenePlexus@3499a60 -flask-compress==1.* +geneplexus @ git+https://github.com/krishnanlab/PyGenePlexus@5722d9b +flask-compress==1.* \ No newline at end of file