diff --git a/context/.storybook/preview.tsx b/context/.storybook/preview.tsx index 3200699b2c..a7b18545da 100644 --- a/context/.storybook/preview.tsx +++ b/context/.storybook/preview.tsx @@ -9,7 +9,6 @@ import '@fontsource-variable/inter/files/inter-latin-standard-normal.woff2'; enableMapSet(); -const allowConditions = [(url) => String(url).endsWith('thumbnail.jpg')]; initialize({ serviceWorker: { options: { 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.stories.tsx b/context/app/static/js/api/scfind/useCellTypeMarkers.stories.tsx new file mode 100644 index 0000000000..5be044f209 --- /dev/null +++ b/context/app/static/js/api/scfind/useCellTypeMarkers.stories.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack, FormControl, TextField } from '@mui/material'; +import { cellTypes } from 'js/components/genes/constants'; +import { http, passthrough } from 'msw'; +import useCellTypeMarkers, { CellTypeMarkersParams } from './useCellTypeMarkers'; + +import { SCFIND_BASE } from './utils'; + +function CellTypeMarkersControl(params: CellTypeMarkersParams) { + const result = useCellTypeMarkers(params); + return ( + + Cell Type Markers + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/CellTypeMarkers', + component: CellTypeMarkersControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + cellTypes: { + control: { + type: 'text', + defaultValue: 'immature B cell', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + backgroundCellTypes: { + control: { + type: 'text', + }, + }, + backgroundAnnotationNames: { + control: { + type: 'text', + }, + }, + topK: { + control: { + type: 'number', + defaultValue: 10, + }, + }, + sortField: { + control: { + type: 'text', + defaultValue: 'f1', + }, + }, + includePrefix: { + control: { + type: 'boolean', + defaultValue: true, + }, + }, + }, +}; + +type Story = StoryObj; + +export const CellTypeMarkers: Story = { + args: { + cellTypes: 'immature B cell', + }, +}; + +export default meta; 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..5707deb256 --- /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; +} + +export 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.stories.tsx b/context/app/static/js/api/scfind/useCellTypeNames.stories.tsx new file mode 100644 index 0000000000..63d61121a2 --- /dev/null +++ b/context/app/static/js/api/scfind/useCellTypeNames.stories.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useCellTypeNames, { CellTypeNamesParams } from './useCellTypeNames'; + +import { SCFIND_BASE } from './utils'; + +function CellTypeNamesControl(params: CellTypeNamesParams) { + const result = useCellTypeNames(params); + return ( + + Cell Type Names + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/CellTypeNames', + component: CellTypeNamesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + annotationNames: { + control: { + type: 'text', + }, + }, + }, +}; + +type Story = StoryObj; + +export const CellTypeNames: Story = { + args: {}, +}; + +export default meta; 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..2d60688e29 --- /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; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useEvaluateMarkers.stories.tsx new file mode 100644 index 0000000000..8ec937d9a8 --- /dev/null +++ b/context/app/static/js/api/scfind/useEvaluateMarkers.stories.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useEvaluateMarkers, { EvaluateMarkersParams } from './useEvaluateMarkers'; + +import { SCFIND_BASE } from './utils'; + +function EvaluateMarkersControl(params: EvaluateMarkersParams) { + const result = useEvaluateMarkers(params); + return ( + + Evaluate Markers + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/EvaluateMarkers', + component: EvaluateMarkersControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + geneList: { + control: { + type: 'text', + }, + }, + cellTypes: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + backgroundCellTypes: { + control: { + type: 'text', + }, + }, + backgroundAnnotationNames: { + control: { + type: 'text', + }, + }, + sortField: { + control: { + type: 'text', + }, + }, + includePrefix: { + control: { + type: 'boolean', + }, + }, + }, +}; + +type Story = StoryObj; + +export const EvaluateMarkers: Story = { + args: {}, +}; + +export default meta; 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..46e9c8521d --- /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'; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useFindCellTypeSpecificities.stories.tsx new file mode 100644 index 0000000000..1ca85cb47d --- /dev/null +++ b/context/app/static/js/api/scfind/useFindCellTypeSpecificities.stories.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useFindCellTypeSpecificities, { FindCellTypeSpecificitiesParams } from './useFindCellTypeSpecificities'; + +import { SCFIND_BASE } from './utils'; + +function FindCellTypeSpecificitiesControl(params: FindCellTypeSpecificitiesParams) { + const result = useFindCellTypeSpecificities(params); + return ( + + Find Cell Type Specificities + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/FindCellTypeSpecificities', + component: FindCellTypeSpecificitiesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + geneList: { + control: { + type: 'text', + }, + }, + cellTypes: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + backgroundCellTypes: { + control: { + type: 'text', + }, + }, + backgroundAnnotationNames: { + control: { + type: 'text', + }, + }, + sortField: { + control: { + type: 'text', + }, + }, + includePrefix: { + control: { + type: 'boolean', + }, + }, + }, +}; + +type Story = StoryObj; + +export const EvaluateMarkers: Story = { + args: {}, +}; + +export default meta; 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..9ed32d69fd --- /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'; + +export interface FindCellTypeSpecificitiesParams { + geneList?: string | string[]; + annotationNames?: AnnotationNamesList; + minCells?: number; + minFraction?: number; +} + +type EvaluateMarkersKey = string; + +interface FindCellTypeSpecificitiesResponse { + evaluateMarkers: unknown; +} + +export function createCellTypeSpecificitiesKey({ + geneList, + annotationNames, + minCells, + minFraction, +}: FindCellTypeSpecificitiesParams): 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: FindCellTypeSpecificitiesParams) { + const key = createCellTypeSpecificitiesKey(params); + return useSWR(key, (url) => fetcher({ url })); +} diff --git a/context/app/static/js/api/scfind/useFindGeneSignatures.stories.tsx b/context/app/static/js/api/scfind/useFindGeneSignatures.stories.tsx new file mode 100644 index 0000000000..e6b1313925 --- /dev/null +++ b/context/app/static/js/api/scfind/useFindGeneSignatures.stories.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useFindGeneSignatures, { FindGeneSignaturesParams } from './useFindGeneSignatures'; + +import { SCFIND_BASE } from './utils'; + +function FindGeneSignaturesControl(params: FindGeneSignaturesParams) { + const result = useFindGeneSignatures(params); + return ( + + Find Gene Signatures + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/FindGeneSignatures', + component: FindGeneSignaturesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + cellTypes: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + minCells: { + control: { + type: 'number', + }, + }, + minFraction: { + control: { + type: 'number', + }, + }, + }, +}; + +type Story = StoryObj; + +export const FindGeneSignatures: Story = { + args: {}, +}; + +export default meta; 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..1ed102362b --- /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'; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useFindHouseKeepingGenes.stories.tsx new file mode 100644 index 0000000000..91f0e3d169 --- /dev/null +++ b/context/app/static/js/api/scfind/useFindHouseKeepingGenes.stories.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useFindHousekeepingGenes, { FindHouseKeepingGenesParams } from './useFindHouseKeepingGenes'; + +import { SCFIND_BASE } from './utils'; + +function FindHousekeepingGenes(params: FindHouseKeepingGenesParams) { + const result = useFindHousekeepingGenes(params); + return ( + + Find Housekeeping Genes + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/FindHousekeepingGenes', + component: FindHousekeepingGenes, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + cellTypes: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + minRecall: { + control: { + type: 'number', + }, + }, + maxGenes: { + control: { + type: 'number', + }, + }, + }, +}; + +type Story = StoryObj; + +export const FindGeneSignatures: Story = { + args: {}, +}; + +export default meta; 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..b42d92fbea --- /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'; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useFindSimilarGenes.stories.tsx new file mode 100644 index 0000000000..6d26eca798 --- /dev/null +++ b/context/app/static/js/api/scfind/useFindSimilarGenes.stories.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useFindSimilarGenes, { FindSimilarGenesParams } from './useFindSimilarGenes'; + +import { SCFIND_BASE } from './utils'; + +function FindSimilarGenesControl(params: FindSimilarGenesParams) { + const result = useFindSimilarGenes(params); + return ( + + Find Similar Genes + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/FindSimilarGenes', + component: FindSimilarGenesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + cellTypes: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + minRecall: { + control: { + type: 'number', + }, + }, + maxGenes: { + control: { + type: 'number', + }, + }, + }, +}; + +type Story = StoryObj; + +export const FindSimilarGenes: Story = { + args: {}, +}; + +export default meta; 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..d20de10a4d --- /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'; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useFindTissueSpecificities.stories.tsx new file mode 100644 index 0000000000..2a80ebea8d --- /dev/null +++ b/context/app/static/js/api/scfind/useFindTissueSpecificities.stories.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useFindTissueSpecificities, { FindTissueSpecificitiesParams } from './useFindTissueSpecificities'; + +import { SCFIND_BASE } from './utils'; + +function FindTissueSpecificitiesControl(params: FindTissueSpecificitiesParams) { + const result = useFindTissueSpecificities(params); + return ( + + Find Tissue Specificities + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/FindTissueSpecificities', + component: FindTissueSpecificitiesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + geneList: { + control: { + type: 'text', + }, + }, + minCells: { + control: { + type: 'number', + }, + }, + }, +}; + +type Story = StoryObj; + +export const FindTissueSpecificities: Story = { + args: {}, +}; + +export default meta; 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..a69e4218d2 --- /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'; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useHyperQueryCellTypes.stories.tsx new file mode 100644 index 0000000000..b66d99ddd6 --- /dev/null +++ b/context/app/static/js/api/scfind/useHyperQueryCellTypes.stories.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useHyperQueryCellTypes, { CellTypeNamesParams } from './useHyperQueryCellTypes'; + +import { SCFIND_BASE } from './utils'; + +function HyperQueryCellTypesControl(params: CellTypeNamesParams) { + const result = useHyperQueryCellTypes(params); + return ( + + HyperQuery Cell Types + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/HyperQueryCellTypes', + component: HyperQueryCellTypesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + geneList: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + includePrefix: { + control: { + type: 'boolean', + }, + }, + }, +}; + +type Story = StoryObj; + +export const HyperQueryCellTypes: Story = { + args: {}, +}; + +export default meta; 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..03b97235af --- /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'; + +export 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.stories.tsx b/context/app/static/js/api/scfind/useMarkerGenes.stories.tsx new file mode 100644 index 0000000000..a9beda1d7b --- /dev/null +++ b/context/app/static/js/api/scfind/useMarkerGenes.stories.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography, Stack } from '@mui/material'; +import { http, passthrough } from 'msw'; +import useMarkerGenes, { MarkerGenesParams } from './useMarkerGenes'; + +import { SCFIND_BASE } from './utils'; + +function MarkerGenesControl(params: MarkerGenesParams) { + const result = useMarkerGenes(params); + return ( + + Marker Genes + Params: +
{JSON.stringify(params, null, 2)}
+ Results: +
{JSON.stringify(result, null, 2)}
+
+ ); +} + +const meta: Meta = { + title: 'SCFind/MarkerGenes', + component: MarkerGenesControl, + parameters: { + msw: { + handlers: [ + http.get(`${SCFIND_BASE}/*`, () => { + return passthrough(); + }), + ], + }, + }, + argTypes: { + markerGenes: { + control: { + type: 'text', + }, + }, + datasetName: { + control: { + type: 'text', + }, + }, + annotationNames: { + control: { + type: 'text', + }, + }, + exhaustive: { + control: { + type: 'boolean', + }, + }, + support_cutoff: { + control: { + type: 'number', + }, + }, + }, +}; + +type Story = StoryObj; + +export const MarkerGenes: Story = { + args: {}, +}; + +export default meta; 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..1e1ecee7ad --- /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[]; + +export interface MarkerGenesParams { + markerGenes: string | string[]; + datasetName?: string; + annotationNames?: AnnotationNamesList; + exhaustive?: boolean; + supportCutoff?: number; +} + +function createMarkerGenesKey({ + markerGenes, + datasetName, + annotationNames, + exhaustive, + supportCutoff, +}: 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: supportCutoff !== undefined ? String(supportCutoff) : 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..88f8a46569 --- /dev/null +++ b/context/app/static/js/api/scfind/utils.ts @@ -0,0 +1,43 @@ +import { AnnotationNamesList } from './types'; + +// Current URL for SCFind as of 2/12/2025 +// PROD +export const SCFIND_BASE = 'http://scfind.hubmapconsortium.org/api'; +// DEV +// export const SCFIND_BASE = 'http://18.219.167.106:8080/api'; + +// TODO: Transfer to app config + +/** + * 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}]`; +}