diff --git a/context/app/static/js/api/scfind/scfind.test.ts b/context/app/static/js/api/scfind/scfind.test.ts new file mode 100644 index 0000000000..546e9272f9 --- /dev/null +++ b/context/app/static/js/api/scfind/scfind.test.ts @@ -0,0 +1,61 @@ +import { createScfindKey, annotationNamesToGetParams, SCFIND_BASE } from './utils'; + +describe('createScfindKey', () => { + function expectURLIsValid(key: string) { + expect(() => new URL(key)).not.toThrow(); + } + + it('should use the appropriate scfind base URL', () => { + const key = createScfindKey('endpoint', {}); + expect(key).toContain(SCFIND_BASE); + expectURLIsValid(key); + }); + + it.each(['endpoint1', 'endpoint2', 'endpoint3', 'my-weird-endpoint'])( + 'should use the appropriate provided endpoint', + (endpoint) => { + const key = createScfindKey(endpoint, {}); + expect(key).toContain(endpoint); + expectURLIsValid(key); + }, + ); + + it.each([ + { + params: { + param1: 'value1', + param2: 'value2', + param3: undefined, + }, + definedKeys: ['param1', 'param2'], + undefinedKeys: ['param3'], + }, + ])('should filter out undefined passed params', ({ params, definedKeys, undefinedKeys }) => { + const key = createScfindKey('endpoint', params); + definedKeys.forEach((k) => { + expect(key.includes(k)); + }); + undefinedKeys.forEach((k) => { + expect(!key.includes(k)); + }); + expectURLIsValid(key); + }); +}); + +describe('annotationNamesToGetParams', () => { + it('should return undefined if annotationNames is undefined', () => { + expect(annotationNamesToGetParams(undefined)).toBeUndefined(); + }); + + it.each([ + { + obj: [ + { Organ: 'organ1', Tissue: 'tissue1' }, + { Organ: 'organ1', Tissue: 'tissue2' }, + ], + expected: '[{"Organ": "organ1", "Tissue": "tissue1"},{"Organ": "organ1", "Tissue": "tissue2"}]', + }, + ])('should format annotation names correctly', ({ obj, expected }) => { + expect(annotationNamesToGetParams(obj)).toBe(expected); + }); +}); diff --git a/context/app/static/js/api/scfind/types.ts b/context/app/static/js/api/scfind/types.ts new file mode 100644 index 0000000000..4115597cf0 --- /dev/null +++ b/context/app/static/js/api/scfind/types.ts @@ -0,0 +1,6 @@ +interface AnnotationNames { + Organ: string; + Tissue: string; +} + +export type AnnotationNamesList = AnnotationNames[]; diff --git a/context/app/static/js/api/scfind/useCellTypeMarkers.ts b/context/app/static/js/api/scfind/useCellTypeMarkers.ts new file mode 100644 index 0000000000..cf3eb1eca1 --- /dev/null +++ b/context/app/static/js/api/scfind/useCellTypeMarkers.ts @@ -0,0 +1,61 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { AnnotationNamesList } from './types'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; + +interface CellTypeMarkerInfo { + cellType: string; + f1: number; + fn: number; + fp: number; + genes: string; + precision: number; + recall: number; + tp: number; +} + +interface CellTypeMarkersParams { + cellTypes: string | string[]; + annotationNames: AnnotationNamesList; + backgroundCellTypes?: string[]; + backgroundAnnotationNames?: AnnotationNamesList; + topK?: number; + sortField?: keyof CellTypeMarkerInfo; + includePrefix?: boolean; +} + +type CellTypeMarkersKey = string; + +interface CellTypeMarkersResponse { + cellTypeMarkers: CellTypeMarkerInfo[]; +} + +export function createCellTypeMarkersKey({ + cellTypes, + annotationNames, + backgroundCellTypes, + backgroundAnnotationNames, + topK, + sortField, + includePrefix, +}: CellTypeMarkersParams): CellTypeMarkersKey { + return createScfindKey('cellTypeMarkers', { + cell_types: Array.isArray(cellTypes) ? cellTypes.join(',') : cellTypes, + annotation_names: annotationNames ? annotationNamesToGetParams(annotationNames) : undefined, + background_cell_types: Array.isArray(backgroundCellTypes) ? backgroundCellTypes.join(',') : backgroundCellTypes, + background_annotation_names: annotationNamesToGetParams(backgroundAnnotationNames), + top_k: topK ? topK.toString() : undefined, + include_prefix: includePrefix ? String(includePrefix) : undefined, + sort_field: sortField, + }); +} + +export default function useCellTypeMarkers({ + topK = 10, + sortField = 'f1', + includePrefix = true, + ...params +}: CellTypeMarkersParams) { + const key = createCellTypeMarkersKey({ topK, sortField, includePrefix, ...params }); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useCellTypeNames.ts b/context/app/static/js/api/scfind/useCellTypeNames.ts new file mode 100644 index 0000000000..9dd5a84be5 --- /dev/null +++ b/context/app/static/js/api/scfind/useCellTypeNames.ts @@ -0,0 +1,25 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +type CellTypeNamesKey = string; + +interface CellTypeNamesParams { + annotationNames: AnnotationNamesList; +} + +interface CellTypeNamesResponse { + cellTypeNames: string[]; +} + +export function createCellTypeNamesKey(annotationNames?: AnnotationNamesList): CellTypeNamesKey { + return createScfindKey('cellTypeNames', { + annotation_names: annotationNames ? annotationNamesToGetParams(annotationNames) : undefined, + }); +} + +export default function useCellTypeNames({ annotationNames }: CellTypeNamesParams) { + const key = createCellTypeNamesKey(annotationNames); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useEvaluateMarkers.ts b/context/app/static/js/api/scfind/useEvaluateMarkers.ts new file mode 100644 index 0000000000..8c367d2dfa --- /dev/null +++ b/context/app/static/js/api/scfind/useEvaluateMarkers.ts @@ -0,0 +1,45 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +interface EvaluateMarkersParams { + geneList: string | string[]; + cellTypes: string | string[]; + annotationNames?: AnnotationNamesList; + backgroundCellTypes?: string[]; + backgroundAnnotationNames?: AnnotationNamesList; + sortField?: string; + includePrefix?: boolean; +} + +type EvaluateMarkersKey = string; + +interface EvaluateMarkersResponse { + evaluateMarkers: unknown; +} + +export function createCellTypeMarkersKey({ + geneList, + cellTypes, + annotationNames, + backgroundCellTypes, + backgroundAnnotationNames, + sortField, + includePrefix, +}: EvaluateMarkersParams): EvaluateMarkersKey { + return createScfindKey('evaluateMarkers', { + gene_list: Array.isArray(geneList) ? geneList.join(',') : geneList, + cell_types: Array.isArray(cellTypes) ? cellTypes.join(',') : cellTypes, + annotation_names: annotationNamesToGetParams(annotationNames), + background_cell_types: Array.isArray(backgroundCellTypes) ? backgroundCellTypes.join(',') : backgroundCellTypes, + background_annotation_names: annotationNamesToGetParams(backgroundAnnotationNames), + sort_field: sortField, + include_prefix: includePrefix !== undefined ? String(includePrefix) : undefined, + }); +} + +export default function useEvaluateMarkers(params: EvaluateMarkersParams) { + const key = createCellTypeMarkersKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useFindCellTypeSpecificities.ts b/context/app/static/js/api/scfind/useFindCellTypeSpecificities.ts new file mode 100644 index 0000000000..1d847bd387 --- /dev/null +++ b/context/app/static/js/api/scfind/useFindCellTypeSpecificities.ts @@ -0,0 +1,36 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +interface EvaluateMarkersParams { + geneList?: string | string[]; + annotationNames?: AnnotationNamesList; + minCells?: number; + minFraction?: number; +} + +type EvaluateMarkersKey = string; + +interface EvaluateMarkersResponse { + evaluateMarkers: unknown; +} + +export function createCellTypeSpecificitiesKey({ + geneList, + annotationNames, + minCells, + minFraction, +}: EvaluateMarkersParams): EvaluateMarkersKey { + return createScfindKey('findCellTypeSpecificities', { + annotation_names: annotationNamesToGetParams(annotationNames), + gene_list: Array.isArray(geneList) ? geneList.join(',') : geneList, + min_cells: minCells ? String(minCells) : undefined, + min_fraction: minFraction ? String(minFraction) : undefined, + }); +} + +export default function useFindCellTypeSpecificities(params: EvaluateMarkersParams) { + const key = createCellTypeSpecificitiesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useFindGeneSignatures.ts b/context/app/static/js/api/scfind/useFindGeneSignatures.ts new file mode 100644 index 0000000000..48e1829541 --- /dev/null +++ b/context/app/static/js/api/scfind/useFindGeneSignatures.ts @@ -0,0 +1,36 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +interface FindGeneSignaturesParams { + cellTypes?: string | string[]; + annotationNames?: AnnotationNamesList; + minCells?: number; + minFraction?: number; +} + +type FindGeneSignaturesKey = string; + +interface FindGeneSignaturesResponse { + evaluateMarkers: unknown; +} + +export function createFindGeneSignaturesKey({ + cellTypes, + annotationNames, + minCells, + minFraction, +}: FindGeneSignaturesParams): FindGeneSignaturesKey { + return createScfindKey('findGeneSignatures', { + cell_types: Array.isArray(cellTypes) ? cellTypes.join(',') : cellTypes, + annotation_names: annotationNamesToGetParams(annotationNames), + min_cells: minCells ? String(minCells) : undefined, + min_fraction: minFraction ? String(minFraction) : undefined, + }); +} + +export default function useFindGeneSignatures(params: FindGeneSignaturesParams) { + const key = createFindGeneSignaturesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useFindHouseKeepingGenes.ts b/context/app/static/js/api/scfind/useFindHouseKeepingGenes.ts new file mode 100644 index 0000000000..74f8badb1e --- /dev/null +++ b/context/app/static/js/api/scfind/useFindHouseKeepingGenes.ts @@ -0,0 +1,36 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +interface FindHouseKeepingGenesParams { + cellTypes?: string | string[]; + annotationNames?: AnnotationNamesList; + minRecall?: number; + maxGenes?: number; +} + +type FindHouseKeepingGenesKey = string; + +interface FindHouseKeepingGenesResponse { + findHouseKeepingGenes: unknown; +} + +export function createFindHouseKeepingGenesKey({ + cellTypes, + annotationNames, + minRecall, + maxGenes, +}: FindHouseKeepingGenesParams): FindHouseKeepingGenesKey { + return createScfindKey('findHouseKeepingGenes', { + cell_types: Array.isArray(cellTypes) ? cellTypes.join(',') : cellTypes, + annotation_names: annotationNamesToGetParams(annotationNames), + min_recall: minRecall ? String(minRecall) : undefined, + max_genes: maxGenes ? String(maxGenes) : undefined, + }); +} + +export default function useFindHouseKeepingGenes(params: FindHouseKeepingGenesParams) { + const key = createFindHouseKeepingGenesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useFindSimilarGenes.ts b/context/app/static/js/api/scfind/useFindSimilarGenes.ts new file mode 100644 index 0000000000..05b687f134 --- /dev/null +++ b/context/app/static/js/api/scfind/useFindSimilarGenes.ts @@ -0,0 +1,36 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +interface FindSimilarGenesParams { + cellTypes?: string | string[]; + annotationNames?: AnnotationNamesList; + minRecall?: number; + maxGenes?: number; +} + +type FindSimilarGenesKey = string; + +interface FindSimilarGenesResponse { + evaluateMarkers: unknown; +} + +export function createFindSimilarGenesKey({ + cellTypes, + annotationNames, + minRecall, + maxGenes, +}: FindSimilarGenesParams): FindSimilarGenesKey { + return createScfindKey('findSimilarGenes', { + cell_types: Array.isArray(cellTypes) ? cellTypes.join(',') : cellTypes, + annotation_names: annotationNamesToGetParams(annotationNames), + min_recall: minRecall ? String(minRecall) : undefined, + max_genes: maxGenes ? String(maxGenes) : undefined, + }); +} + +export default function useFindSimilarGenes(params: FindSimilarGenesParams) { + const key = createFindSimilarGenesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useFindTissueSpecificities.ts b/context/app/static/js/api/scfind/useFindTissueSpecificities.ts new file mode 100644 index 0000000000..871e6821ef --- /dev/null +++ b/context/app/static/js/api/scfind/useFindTissueSpecificities.ts @@ -0,0 +1,29 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey } from './utils'; + +interface FindTissueSpecificitiesParams { + geneList?: string | string[]; + minCells?: number; +} + +type FindTissueSpecificitiesKey = string; + +interface FindTissueSpecificitiesResponse { + evaluateMarkers: unknown; +} + +export function FindTissueSpecificitiesKey({ + geneList, + minCells, +}: FindTissueSpecificitiesParams): FindTissueSpecificitiesKey { + return createScfindKey('findTissueSpecificities', { + gene_list: Array.isArray(geneList) ? geneList.join(',') : geneList, + min_cells: minCells ? String(minCells) : undefined, + }); +} + +export default function useFindTissueSpecificities(params: FindTissueSpecificitiesParams) { + const key = FindTissueSpecificitiesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useHyperQueryCellTypes.ts b/context/app/static/js/api/scfind/useHyperQueryCellTypes.ts new file mode 100644 index 0000000000..c11ffad696 --- /dev/null +++ b/context/app/static/js/api/scfind/useHyperQueryCellTypes.ts @@ -0,0 +1,33 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +interface CellTypeNamesParams { + geneList: string | string[]; + annotationNames: AnnotationNamesList; + includePrefix: boolean; +} + +interface CellTypeNamesResponse { + cellTypeNames: string[]; +} + +type CellTypeNamesKey = string; + +export function createCellTypeNamesKey({ + geneList, + annotationNames, + includePrefix, +}: CellTypeNamesParams): CellTypeNamesKey { + return createScfindKey('hyperQueryCellTypes', { + gene_list: Array.isArray(geneList) ? geneList.join(',') : geneList, + annotation_names: annotationNamesToGetParams(annotationNames), + include_prefix: includePrefix ? 'true' : 'false', + }); +} + +export default function useHyperQueryCellTypes(params: CellTypeNamesParams) { + const key = createCellTypeNamesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useMarkerGenes.ts b/context/app/static/js/api/scfind/useMarkerGenes.ts new file mode 100644 index 0000000000..7640d8046a --- /dev/null +++ b/context/app/static/js/api/scfind/useMarkerGenes.ts @@ -0,0 +1,37 @@ +import useSWR from 'swr'; +import { fetcher } from 'js/helpers/swr'; +import { createScfindKey, annotationNamesToGetParams } from './utils'; +import { AnnotationNamesList } from './types'; + +type MarkerGenesKey = string; + +type MarkerGenesResponse = object[]; + +interface MarkerGenesParams { + markerGenes: string | string[]; + datasetName?: string; + annotationNames?: AnnotationNamesList; + exhaustive?: boolean; + support_cutoff?: number; +} + +function createMarkerGenesKey({ + markerGenes, + datasetName, + annotationNames, + exhaustive, + support_cutoff, +}: MarkerGenesParams): MarkerGenesKey { + return createScfindKey('markerGenes', { + marker_genes: Array.isArray(markerGenes) ? markerGenes.join(',') : markerGenes, + dataset_name: datasetName, + annotation_names: annotationNamesToGetParams(annotationNames), + exhaustive: exhaustive !== undefined ? String(exhaustive) : undefined, + support_cutoff: support_cutoff !== undefined ? String(support_cutoff) : undefined, + }); +} + +export default function useMarkerGenes(params: MarkerGenesParams) { + const key = createMarkerGenesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/utils.ts b/context/app/static/js/api/scfind/utils.ts new file mode 100644 index 0000000000..333062ba33 --- /dev/null +++ b/context/app/static/js/api/scfind/utils.ts @@ -0,0 +1,38 @@ +import { AnnotationNamesList } from './types'; + +// Current URL for SCFind as of 1/13/2025 +export const SCFIND_BASE = 'http://44.197.120.34/api'; + +/** + * Helper method to form URLs for SCFind API requests + * + * @param endpoint - The endpoint to hit + * @param params - The parameters to include in the URL + */ +export function createScfindKey(endpoint: string, params: Record): string { + const urlParams = new URLSearchParams(); + // Filter out undefined values from url params + Object.entries(params) + .filter(([, value]) => value) + .forEach(([key, value]) => urlParams.append(key, value!)); + const fullUrl = new URL(`${SCFIND_BASE}/${endpoint}?${urlParams.toString()}`); + return fullUrl.toString(); +} + +/** + * Helper method to format annotation names for SCFind API requests + * Formats annotation names to [ + * {"Organ": "organ1", "Tissue": "tissue1"}, + * {"Organ": "organ1", "Tissue": "tissue2"}, + * ]. + * + * @param annotationNames - The annotation names to format + * @returns The formatted annotation names, or undefined if annotationNames is undefined + */ +export function annotationNamesToGetParams(annotationNames?: AnnotationNamesList): string | undefined { + if (!annotationNames) { + return undefined; + } + const list = annotationNames.map(({ Organ, Tissue }) => `{"Organ": "${Organ}", "Tissue": "${Tissue}"}`).join(','); + return `[${list}]`; +} diff --git a/cspell.json b/cspell.json index ee2f482f17..7735e1519d 100644 --- a/cspell.json +++ b/cspell.json @@ -3,7 +3,9 @@ "ignorePaths": [], "dictionaryDefinitions": [], "dictionaries": [], - "words": [], + "words": [ + "SCFIND" + ], "ignoreWords": [], "import": [] }