diff --git a/frontend/packages/@depmap/api/src/legacyPortalAPI/index.ts b/frontend/packages/@depmap/api/src/legacyPortalAPI/index.ts index f34ba2a9d..058e3b6e8 100644 --- a/frontend/packages/@depmap/api/src/legacyPortalAPI/index.ts +++ b/frontend/packages/@depmap/api/src/legacyPortalAPI/index.ts @@ -7,6 +7,7 @@ import * as data_page from "./resources/data_page"; import * as download from "./resources/download"; import * as entity_summary from "./resources/entity_summary"; import * as genetea from "./resources/genetea"; +import * as experimental_genetea from "./resources/experimental_genetea"; import * as interactive from "./resources/interactive"; import * as tda from "./resources/tda"; import * as misc from "./resources/misc"; @@ -24,6 +25,7 @@ export const legacyPortalAPI = { ...interactive, ...tda, ...misc, + ...experimental_genetea, }; type Api = typeof legacyPortalAPI; diff --git a/frontend/packages/@depmap/api/src/legacyPortalAPI/resources/experimental_genetea.ts b/frontend/packages/@depmap/api/src/legacyPortalAPI/resources/experimental_genetea.ts new file mode 100644 index 000000000..fb892c15b --- /dev/null +++ b/frontend/packages/@depmap/api/src/legacyPortalAPI/resources/experimental_genetea.ts @@ -0,0 +1,277 @@ +import qs from "qs"; +import { enabledFeatures } from "@depmap/globals"; +import { getJson } from "../client"; +import { GeneTeaEnrichedTerms } from "@depmap/types/src/experimental_genetea"; + +// ❌ ❌ ❌ WARNING: THIS IS EXPERIMENTAL AND WILL LIKELY CHANGE ❌ ❌ ❌ +// DO NOT USE THIS FOR ANYTHING OTHER THAN THE NEW GENETEA TEA PARTY PAGE!!!!! + +// Do not use in production! For local development only. +const toCorsProxyUrl = (geneTeaUrl: string, params: object) => { + const query = qs.stringify(params, { arrayFormat: "repeat" }); + const url = `https://cds.team/${geneTeaUrl}/?${query}`; + return "https://corsproxy.io/?" + encodeURIComponent(url); +}; + +export async function fetchGeneTeaEnrichmentExperimental( + plotSelections: string[] | null, + genes: string[], + doGroupTerms: boolean, + sortBy: "Significance" | "Effect Size", + maxFDR: number, + maxTopTerms: number | null, + maxMatchingOverall: number | null, + minMatchingQuery: number + // effectSizeThreshold: number, +): Promise { + if (!enabledFeatures.gene_tea) { + throw new Error("GeneTea is not supported in this environment!"); + } + + const geneTeaUrl = "genetea-api/enriched-terms-v2"; + + let params: any = { + gene_list: genes, + group_terms: doGroupTerms, + include_plotting_payload: "true", + sort_by: sortBy, + max_fdr: maxFDR, + min_genes: minMatchingQuery || -1, + max_n_genes: maxMatchingOverall, + n: plotSelections?.length === 0 ? maxTopTerms || -1 : -1, + }; + + if (plotSelections) { + params = { ...params, plot_selections: plotSelections }; + } + console.log("For Sanity Checking", params); + + interface RawResponse { + // TODO: Give the user feedback when some genes are invalid. + valid_genes: string[]; + invalid_genes: string[]; + total_n_enriched_terms: number; + total_n_term_groups: number; + enriched_terms: { + Term: string[]; + "Matching Genes in List": string[]; + "n Matching Genes Overall": number[]; + "n Matching Genes in List": number[]; + "p-val": number[]; + FDR: number[]; + Stopword: boolean[]; + Synonyms: (string | null)[]; + "Total Info": number[]; + "Effect Size": number[]; + "Term Group": string[]; + "-log10 FDR": number[]; + "Clipped Term": string[]; + "Clipped Synonyms": (string | null)[]; + "Clipped Matching Genes in List": string[]; + }; + plotting_payload: { + groupby: "Term" | "Term Group"; + term_cluster: { + Term: string[] | null[]; + "Term Group": string[] | null[]; + Cluster: number[]; + Order: number[]; + }; + gene_cluster: { + Gene: string[]; + Cluster: number[]; + Order: number[]; + }; + term_to_entity: any; // TODO update this type + frequent_terms: { + Term: string[]; + "Matching Genes in List": string[]; + "n Matching Genes Overall": number[]; + "n Matching Genes in List": number[]; + "p-val": number[]; + FDR: number[]; + Stopword: boolean[]; + Synonyms: (string | null)[]; + "Total Info": number[]; + "Effect Size": number[]; + }; + all_enriched_terms: { + Term: string[]; + "Matching Genes in List": string[]; + "n Matching Genes Overall": number[]; + "n Matching Genes in List": number[]; + "p-val": number[]; + FDR: number[]; + Stopword: boolean[]; + Synonyms: (string | null)[]; + "Total Info": number[]; + "Effect Size": number[]; + "Term Group": string[]; + }; + }; + } + + const body = + process.env.NODE_ENV === "development" + ? await getJson( + toCorsProxyUrl(geneTeaUrl, params), + undefined, + { credentials: "omit" } + ) + : await getJson(`/../../${geneTeaUrl}/`, params); + + // `enriched_terms` can be null when there are no relevant terms. We'll + // return a wrapper object to distinguish this from some kind of error. + if (body.enriched_terms === null) { + return { + groupby: "Term", + validGenes: [], + invalidGenes: [], + enrichedTerms: null, + totalNEnrichedTerms: 0, + totalNTermGroups: 0, + termCluster: null, + geneCluster: null, + termToEntity: null, + frequentTerms: null, + allEnrichedTerms: null, + }; + } + + const plottingPayload = body.plotting_payload; + const allEt = plottingPayload.all_enriched_terms; + const et = body.enriched_terms; + const enrichedTerms = { + term: et.Term, + fdr: et.FDR, + totalEnrichedTerms: body.total_n_enriched_terms, + totalTermGroups: body.total_n_term_groups, + synonyms: et.Synonyms.map((list) => list?.split(";") || []), + matchingGenesInList: et["Matching Genes in List"], + nMatchingGenesInList: et["n Matching Genes in List"], + nMatchingGenesOverall: et["n Matching Genes Overall"], + termGroup: et["Term Group"], + effectSize: et["Effect Size"], + pVal: et["p-val"], + stopword: et.Stopword, + totalInfo: et["Total Info"], + negLogFDR: et["-log10 FDR"], + clippedTerm: et["Clipped Term"], + clippedSynonyms: et["Clipped Synonyms"].map( + (list) => list?.split(";") || [] + ), + clippedMatchingGenesInList: et["Clipped Matching Genes in List"], + }; + const allEnrichedTerms = { + term: allEt.Term, + fdr: allEt.FDR, + synonyms: allEt.Synonyms.map((list) => list?.split(";") || []), + matchingGenesInList: allEt["Matching Genes in List"], + nMatchingGenesInList: allEt["n Matching Genes in List"], + nMatchingGenesOverall: allEt["n Matching Genes Overall"], + termGroup: allEt["Term Group"], + effectSize: allEt["Effect Size"], + pVal: allEt["p-val"], + stopword: allEt.Stopword, + totalInfo: et["Total Info"], + }; + + const termClusterTermOrGroup = doGroupTerms + ? plottingPayload.term_cluster["Term Group"] + : plottingPayload.term_cluster.Term; + + const termCluster = { + termOrTermGroup: termClusterTermOrGroup as string[], + cluster: plottingPayload.term_cluster.Cluster, + order: plottingPayload.term_cluster.Order, + }; + + const geneCluster = { + gene: plottingPayload.gene_cluster.Gene, + cluster: plottingPayload.gene_cluster.Cluster, + order: plottingPayload.gene_cluster.Order, + }; + + const termToEntity = { + termOrTermGroup: doGroupTerms + ? (plottingPayload.term_to_entity["Term Group"] as string[]) + : (plottingPayload.term_to_entity.Term as string[]), + gene: plottingPayload.term_to_entity.Gene, + count: plottingPayload.term_to_entity.Count, + nTerms: plottingPayload.term_to_entity["n Terms"], + fraction: plottingPayload.term_to_entity.Fraction, + }; + + const frequentTerms = { + term: plottingPayload.frequent_terms.Term, + matchingGenesInList: + plottingPayload.frequent_terms["Matching Genes in List"], + nMatchingGenesOverall: + plottingPayload.frequent_terms["n Matching Genes Overall"], + nMatchingGenesInList: + plottingPayload.frequent_terms["n Matching Genes in List"], + pVal: plottingPayload.frequent_terms["p-val"], + fdr: plottingPayload.frequent_terms.FDR, + stopword: plottingPayload.frequent_terms.Stopword, + synonyms: plottingPayload.frequent_terms.Synonyms.map( + (list) => list?.split(";") || [] + ), + totalInfo: plottingPayload.frequent_terms["Total Info"], + effectSize: plottingPayload.frequent_terms["Effect Size"], + }; + + return { + validGenes: body.valid_genes, + invalidGenes: body.invalid_genes, + totalNEnrichedTerms: body.total_n_enriched_terms, + totalNTermGroups: body.total_n_term_groups, + groupby: plottingPayload.groupby, + enrichedTerms, + termCluster, + geneCluster, + termToEntity, + frequentTerms, + allEnrichedTerms, + }; +} + +export async function fetchGeneTeaTermContextExperimental( + term: string, + genes: string[] +): Promise> { + if (!enabledFeatures.gene_tea) { + throw new Error("GeneTea is not supported in this environment!"); + } + + const geneTeaUrl = "genetea-api/context"; + + const params = { + term, + gene_list: genes, + html: true, + }; + + type RawResponse = + | { + valid_genes: string[]; + invalid_genes: string[]; + remapped_genes: Record; + context: Record; + } + | { message: string }; // error message + + const body = + process.env.NODE_ENV === "development" + ? await getJson( + toCorsProxyUrl(geneTeaUrl, params), + undefined, + { credentials: "omit" } + ) + : await getJson(`/../../${geneTeaUrl}/`, params); + + if ("message" in body) { + throw new Error(body.message); + } + + return body.context; +} diff --git a/frontend/packages/@depmap/types/src/experimental_genetea.ts b/frontend/packages/@depmap/types/src/experimental_genetea.ts new file mode 100644 index 000000000..6e722fccd --- /dev/null +++ b/frontend/packages/@depmap/types/src/experimental_genetea.ts @@ -0,0 +1,83 @@ +export interface GeneTeaTableRow { + term: string; + termGroup: string; + synonyms: string; + matchingGenesInList: string; + nMatchingGenesOverall: number; + nMatchingGenesInList: number; + fdr: number; + effectSize: number; +} + +type GeneSymbol = string; +type FractionMatching = number | null; +type TermOrTermGroup = string; + +export interface HeatmapFormattedData { + x: GeneSymbol[]; + y: TermOrTermGroup[]; + z: FractionMatching[]; + customdata: string[]; // For hover text +} + +export interface BarChartFormattedData { + x: number[]; + y: TermOrTermGroup[]; + customdata: string[]; // For hover text +} + +export interface TermCluster { + termOrTermGroup: string[]; + cluster: number[]; + order: number[]; +} + +export interface GeneCluster { + gene: string[]; + cluster: number[]; + order: number[]; +} + +export interface TermToEntity { + termOrTermGroup: string[]; + gene: string[]; + count: number[]; + nTerms: number[]; + fraction: number[]; +} + +export interface FrequentTerms { + term: string[]; + matchingGenesInList: string[]; + nMatchingGenesOverall: number[]; + nMatchingGenesInList: number[]; + pVal: number[]; + fdr: number[]; + stopword: boolean[]; + synonyms: string[][]; + totalInfo: number[]; + effectSize: number[]; +} + +export interface AllEnrichedTerms extends FrequentTerms { + termGroup: string[]; +} + +export interface EnrichedTerms extends AllEnrichedTerms { + negLogFDR: number[]; + clippedTerm: string[]; +} + +export interface GeneTeaEnrichedTerms { + validGenes: string[]; + invalidGenes: string[]; + totalNEnrichedTerms: number; + totalNTermGroups: number; + groupby: "Term" | "Term Group"; + enrichedTerms: EnrichedTerms | null; + termCluster: TermCluster | null; + geneCluster: GeneCluster | null; + termToEntity: TermToEntity | null; + frequentTerms: FrequentTerms | null; + allEnrichedTerms: AllEnrichedTerms | null; +} diff --git a/frontend/packages/@depmap/wide-table/src/WideTable.tsx b/frontend/packages/@depmap/wide-table/src/WideTable.tsx index 302138e56..9af7c167d 100644 --- a/frontend/packages/@depmap/wide-table/src/WideTable.tsx +++ b/frontend/packages/@depmap/wide-table/src/WideTable.tsx @@ -82,9 +82,11 @@ export interface WideTableColumns { export interface WideTableProps { /** * array of objects, all data for the table (will be what ends up downloaded if - * allowDownloadFromTableData is set to true and the user clicks "download table") + * allowDownloadFromTableData is set to true, prefferedTableDataForDownload is undefined + * and the user clicks "download table") */ data: any[]; + prefferedTableDataForDownload?: any[]; // If set, use this for the download table instead of "data" columns: Array>; // refer to the react-table docs for structure invisibleColumns?: Array; @@ -864,7 +866,8 @@ class WideTable extends React.Component { const dropdownColumnHideShowMenu = this.renderShowHideMenu(); let downloadButton = null; let numberOfRows = null; - const dataToDownload = this.props.data; + const dataToDownload = + this.props.prefferedTableDataForDownload || this.props.data; if (this.props.downloadURL) { downloadButton = ( + + ); + })} + +