diff --git a/apps/ftu-ui/src/assets/TEMP/ftu-cell-summaries.jsonld b/apps/ftu-ui/src/assets/TEMP/ftu-cell-summaries.jsonld index caa66b9c7..7467ca183 100644 --- a/apps/ftu-ui/src/assets/TEMP/ftu-cell-summaries.jsonld +++ b/apps/ftu-ui/src/assets/TEMP/ftu-cell-summaries.jsonld @@ -27,7 +27,7 @@ "@graph": [ { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/kidney-nephron", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-nephron", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ @@ -3746,7 +3746,7 @@ }, { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/kidney-renal-corpuscle", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-renal-corpuscle", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ @@ -6744,7 +6744,7 @@ }, { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/kidney-cortical-collecting-duct", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-cortical-collecting-duct", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ @@ -9756,7 +9756,7 @@ }, { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/kidney-ascending-thin-loop-of-henle", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-ascending-thin-loop-of-henle", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ @@ -11266,7 +11266,7 @@ }, { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/liver-liver-lobule", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_liver-liver-lobule", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ @@ -13485,7 +13485,7 @@ }, { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/lung-pulmonary-alveolus", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_lung-pulmonary-alveolus", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ @@ -16399,7 +16399,7 @@ }, { "@type": "CellSummary", - "cell_source": "https://purl.humanatlas.io/2d-ftu/lung-bronchial-submucosal-gland", + "cell_source": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_lung-bronchial-submucosal-gland", "annotation_method": "Aggregation", "biomarker_type": "gene", "summary": [ diff --git a/apps/ftu-ui/src/assets/TEMP/ftu-datasets.jsonld b/apps/ftu-ui/src/assets/TEMP/ftu-datasets.jsonld index 79b618869..ed5085371 100644 --- a/apps/ftu-ui/src/assets/TEMP/ftu-datasets.jsonld +++ b/apps/ftu-ui/src/assets/TEMP/ftu-datasets.jsonld @@ -30,7 +30,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-nephron", "@type": "Dataset", "label": "snRNA-seq of Three Healthy Human Kidney Tissue", "link": "https://doi.org/10.1038/s41467-021-22368-w", @@ -53,7 +53,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-renal-corpuscle", "@type": "Dataset", "label": "snRNA-seq of Three Healthy Human Kidney Tissue", "link": "https://doi.org/10.1038/s41467-021-22368-w", @@ -76,7 +76,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-cortical-collecting-duct", "@type": "Dataset", "label": "snRNA-seq of Three Healthy Human Kidney Tissue", "link": "https://doi.org/10.1038/s41467-021-22368-w", @@ -99,7 +99,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_kidney-ascending-thin-loop-of-henle", "@type": "Dataset", "label": "snRNA-seq of Three Healthy Human Kidney Tissue", "link": "https://doi.org/10.1038/s41467-021-22368-w", @@ -122,7 +122,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_lung-bronchial-submucosal-gland", "@type": "Dataset", "label": "scRNA Seq of Resected Human Lung Tissue", "link": "https://doi.org/10.1038/s41591-019-0468-5", @@ -149,7 +149,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_lung-pulmonary-alveolus", "@type": "Dataset", "label": "scRNA Seq of Resected Human Lung Tissue", "link": "https://doi.org/10.1038/s41591-019-0468-5", @@ -176,7 +176,7 @@ "@type": "FtuIllustration", "data_sources": [ { - "@id": "https://cns-iu.github.io/hra-cell-type-populations-supporting-information/data/enriched_rui_locations.jsonld#36e76662-60b8-4193-8a70-1bfd4f6938d0_D088_Lung", + "@id": "https://doi.org/10.1038/s41467-021-22368-w#CellSummary_liver-liver-lobule", "@type": "Dataset", "label": "sc Bulk Transcriptomics of Liver Data", "link": "https://doi.org/10.1038/s41598-021-98806-y", diff --git a/libs/components/behavioral/src/lib/biomarker-details/biomarker-details.component.scss b/libs/components/behavioral/src/lib/biomarker-details/biomarker-details.component.scss index 4172c5929..74e379b69 100644 --- a/libs/components/behavioral/src/lib/biomarker-details/biomarker-details.component.scss +++ b/libs/components/behavioral/src/lib/biomarker-details/biomarker-details.component.scss @@ -128,7 +128,6 @@ height: 100%; &.small { - height: fit-content; min-height: fit-content; } } diff --git a/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.scss b/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.scss index 47e87bbb8..8665f5a6f 100644 --- a/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.scss +++ b/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.scss @@ -4,10 +4,9 @@ :host { display: block; - @media (min-height: 60.75rem) { - .table th { - height: 4.5rem !important; - } + .table th, + .table th div { + height: 6.5rem !important; } cdk-virtual-scroll-viewport { @@ -104,10 +103,13 @@ .icon-header { .header-column-text { - transform: rotate(-45deg); + // transform: rotate(-90deg); + writing-mode: vertical-rl; + transform: rotate(180deg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + margin: 0.5rem auto; } } diff --git a/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.ts b/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.ts index e5e7984c9..7f89e11fc 100644 --- a/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.ts +++ b/libs/components/organisms/src/lib/biomarker-table/biomarker-table.component.ts @@ -170,7 +170,7 @@ export class BiomarkerTableComponent implements OnInit, OnCh * @param changes object consisting of change in the Input */ ngOnChanges(changes: SimpleChanges): void { - this.checkDisplayedColumns(); + this.checkDisplayedColumns('columns' in changes); if ('data' in changes || 'illustrationIds' in changes) { this.dataSource.data = this.sortTableData(this.data); } @@ -194,11 +194,11 @@ export class BiomarkerTableComponent implements OnInit, OnCh /** * Checks to see if columns should be updated */ - checkDisplayedColumns(): void { + checkDisplayedColumns(forceUpdate = false): void { const scrollable = this.vscroll.scrollable; const size = scrollable.measureViewportSize('horizontal'); const offset = scrollable.measureScrollOffset('start'); - let shouldUpdate = false; + let shouldUpdate = forceUpdate; if (size !== this.horizontalViewportSize) { this.updateHorizontalViewportSize(size); diff --git a/libs/services/src/lib/ftu-data/ftu-data.impl.ts b/libs/services/src/lib/ftu-data/ftu-data.impl.ts index 5fb8a2b73..d8bc9db22 100644 --- a/libs/services/src/lib/ftu-data/ftu-data.impl.ts +++ b/libs/services/src/lib/ftu-data/ftu-data.impl.ts @@ -11,7 +11,6 @@ import { RAW_CELL_SUMMARIES, RAW_DATASETS, RAW_ILLUSTRATIONS_JSONLD, - RawCellSummary, RawDatasets, RawIllustrationFile, RawIllustrationsJsonld, @@ -59,11 +58,6 @@ const EMPTY_TISSUE_LIBRARY: TissueLibrary = { nodes: {}, }; -/** Capitalizes the first character */ -function capitalize(str: string): string { - return str.slice(0, 1).toUpperCase() + str.slice(1); -} - /** Converts case to title case for organ name */ function titleCase(name: string) { return name @@ -139,10 +133,7 @@ export class FtuDataImplService extends FtuDataService { @returns An Observable that emits an CellSummary array. */ override getCellSummaries(iri: Iri): Observable { - return this.fetchData(iri, 'summaries', RAW_CELL_SUMMARIES).pipe( - map((data) => data?.['@graph']), - map((data) => (data ? this.constructCellSummaries(data) : [])), - ); + return this.fetchData(iri, 'summaries', RAW_CELL_SUMMARIES).pipe(map((data) => data?.['@graph'] ?? [])); } /** @@ -289,68 +280,6 @@ export class FtuDataImplService extends FtuDataService { return results; } - /** - * constructCellSummaries : Formates Cell Summary after fetching the data - * @param data - * @returns - */ - private constructCellSummaries(data: RawCellSummary['@graph']): CellSummary[] { - type SummaryItem = RawCellSummary['@graph'][number]['summary'][number]; - const cellSummaries: CellSummary[] = []; - const defaultBiomarkerLables = ['gene', 'protein', 'lipid']; - const biomarkersPresent = new Set(data.map((summary) => summary.biomarker_type.toLowerCase())); - const expandGenes = (summary: SummaryItem) => - summary.genes.map((gene) => ({ - ...summary, - ...gene, - })); - - data.forEach((summaryGroup) => { - const nestedSummaries = summaryGroup.summary.map(expandGenes); - const summary = nestedSummaries.reduce((acc, items) => acc.concat(items), [] as (typeof nestedSummaries)[number]); - - const cells = summary.map((entry) => ({ - id: entry.cell_id as Iri, - label: entry.cell_label, - })); - - const biomarkers = summary.map((entry) => ({ - id: entry.gene_id as Iri, - label: entry.gene_label, - })); - - const summaries = summary.map((entry) => ({ - cell: entry.cell_id as Iri, - biomarker: entry.gene_id as Iri, - count: entry.count, - percentage: entry.percentage, - meanExpression: entry.mean_expression, - })); - - cellSummaries.push({ - label: `${capitalize(summaryGroup.biomarker_type)} Biomarkers`, - cellSource: summaryGroup.cell_source, - cells, - biomarkers, - summaries, - }); - }); - - defaultBiomarkerLables.forEach((defaultLabel) => { - if (!biomarkersPresent.has(defaultLabel)) { - cellSummaries.push({ - label: `${capitalize(defaultLabel)} Biomarkers`, - cellSource: '', - cells: [], - biomarkers: [], - summaries: [], - }); - } - }); - - return cellSummaries; - } - /** * Constructs tissue library ,forming parent and child nodes * @param items diff --git a/libs/services/src/lib/ftu-data/ftu-data.mock.ts b/libs/services/src/lib/ftu-data/ftu-data.mock.ts index 78932828e..afae6e9c1 100644 --- a/libs/services/src/lib/ftu-data/ftu-data.mock.ts +++ b/libs/services/src/lib/ftu-data/ftu-data.mock.ts @@ -324,25 +324,7 @@ const sourceReferences: SourceReference[] = [ /** * Dummy data extract om Mock Data of tissue mock */ -const CELL_SUMMARY_DATA: CellSummary[] = Object.values(MOCK_SUMMARIES).map((summary) => ({ - label: summary.label, - cellSource: summary.cellSource, - cells: summary.entries.map((entry) => ({ - id: entry.cell.id as Iri, - label: entry.cell.label, - })), - biomarkers: summary.entries.map((entry) => ({ - id: entry.biomarker.id as Iri, - label: entry.biomarker.label, - })), - summaries: summary.entries.map((entry) => ({ - cell: entry.cell.id as Iri, - biomarker: entry.biomarker.id as Iri, - count: entry.count, - percentage: entry.percentage, - meanExpression: entry.meanExpression, - })), -})); +const CELL_SUMMARY_DATA: CellSummary[] = []; /** This class represents a mock implementation of the FtuDataService class. diff --git a/libs/services/src/lib/ftu-data/ftu-data.model.ts b/libs/services/src/lib/ftu-data/ftu-data.model.ts index 1a58c827f..1a3e96925 100644 --- a/libs/services/src/lib/ftu-data/ftu-data.model.ts +++ b/libs/services/src/lib/ftu-data/ftu-data.model.ts @@ -52,14 +52,14 @@ export const CELL = z.object({ /** Zod Schema for a BIOMARKER */ export const BIOMARKER = z.object({ - id: IRI, + id: z.string(), label: z.string(), }); /** Zod Schema for a CELL_SUMMARY_ROW */ export const CELL_SUMMARY_ROW = z.object({ cell: IRI, - biomarker: IRI, + biomarker: z.string(), count: COUNT, percentage: PERCENTAGE, meanExpression: PERCENTAGE, @@ -68,11 +68,25 @@ export const CELL_SUMMARY_ROW = z.object({ /** Zod Schema for a CELL_SUMMARY */ export const CELL_SUMMARY = z.object({ - label: z.string(), - cellSource: z.string(), - cells: CELL.array(), - biomarkers: BIOMARKER.array(), - summaries: CELL_SUMMARY_ROW.array(), + cell_source: IRI, + biomarker_type: z.string(), + summary: z + .object({ + cell_id: IRI, + cell_label: z.string(), + genes: z + .object({ + gene_id: z.string(), + gene_label: z.string(), + ensemble_id: z.string(), + mean_expression: z.number(), + }) + .array(), + count: z.number(), + percentage: z.number(), + dataset_count: z.number().optional(), + }) + .array(), }); /** Zod Schema for a DATA_FILE_REFERENCE */ @@ -155,29 +169,7 @@ export const RAW_DATASETS = z.object({ /** CELL_SUMMARIES zod object reflecting the format in the file*/ export const RAW_CELL_SUMMARIES = z.object({ - '@graph': z - .object({ - cell_source: IRI, - biomarker_type: z.string(), - summary: z - .object({ - cell_id: z.string(), - cell_label: z.string(), - genes: z - .object({ - '@type': z.string(), - gene_id: z.string(), - gene_label: z.string(), - mean_expression: z.number(), - }) - .array(), - count: z.number(), - percentage: z.number(), - dataset_count: z.number().optional(), - }) - .array(), - }) - .array(), + '@graph': CELL_SUMMARY.array(), }); // --------------------------------------- diff --git a/libs/state/src/lib/cell-summary/cell-summary.helpers.spec.ts b/libs/state/src/lib/cell-summary/cell-summary.helpers.spec.ts index 0702c6f55..cf46e668e 100644 --- a/libs/state/src/lib/cell-summary/cell-summary.helpers.spec.ts +++ b/libs/state/src/lib/cell-summary/cell-summary.helpers.spec.ts @@ -1,149 +1,117 @@ -import { Biomarker, CellSummary, Iri } from '@hra-ui/services'; +import { CellSummary, Iri, SourceReference } from '@hra-ui/services'; +import { produce } from 'immer'; +import { combineSummariesByBiomarkerType, computeAggregate, filterSummaries } from './cell-summary.helpers'; -import { computeAggregate, expandRow, getColumnIndex, getRow, mergeCellSummaries } from './cell-summary.helpers'; -import { CellSummaryAggregate, CellSummaryAggregateRow } from './cell-summary.model'; - -describe('Cell Summary Helpers', () => { - const mockBiomarkers: Biomarker[] = [ - { id: 'biomarker1' as Iri, label: 'Biomarker 1' }, - { id: 'biomarker2' as Iri, label: 'Biomarker 2' }, - ]; - - const mockCells = [ - { id: 'cell1' as Iri, label: 'Cell 1' }, - { id: 'cell2' as Iri, label: 'Cell 2' }, - ]; - - const mockSummary: CellSummary = { - label: 'Summary Label', - cellSource: '', - cells: mockCells, - biomarkers: mockBiomarkers, - summaries: [ - { - biomarker: 'Biomarker1' as Iri, - cell: 'cell1' as Iri, - meanExpression: 0.4, - percentage: 50, - count: 10, - dataset_count: 1, - }, - { - biomarker: 'Biomarker2' as Iri, - cell: 'cell1' as Iri, - meanExpression: 0.5, - percentage: 30, - count: 5, - dataset_count: 1, - }, - ], - }; - - const mockSummary2: CellSummary = { - label: 'Label2', - cellSource: '', - cells: mockCells, - biomarkers: mockBiomarkers, - summaries: [ - { - biomarker: 'Biomarker1' as Iri, - cell: 'cell1' as Iri, - meanExpression: 0.4, - percentage: 50, - count: 10, - dataset_count: 1, - }, - { - biomarker: 'Biomarker2' as Iri, - cell: 'cell1' as Iri, - meanExpression: 0.5, - percentage: 30, - count: 5, - dataset_count: 1, - }, - ], - }; +const source1: SourceReference = { + id: 'source1' as Iri, + label: '', + link: '', + title: '', + year: 2000, + authors: [], + doi: '', +}; +const source2 = produce(source1, (draft) => { + draft.id = 'source2' as Iri; +}); - const mockSummary3: CellSummary = { - label: 'Summary Label', - cellSource: '', - cells: mockCells, - biomarkers: mockBiomarkers, - summaries: [ - { - biomarker: 'Biomarker1' as Iri, - cell: 'cell1' as Iri, - meanExpression: 0.4, - percentage: 50, - count: 10, - dataset_count: 1, - }, - ], - }; - describe('getColumnIndex', () => { - it('should return new index for the column for non-exisitng id', () => { - const indexById = new Map(); - const newIndex = getColumnIndex(indexById, ''); +const geneBiomarker = 'gene'; +const cell1 = 'cell1' as Iri; +const cell2 = 'cell2' as Iri; +const gene1 = 'gene1' as Iri; +const ensembleId1 = 'ensemble1'; - expect(newIndex).toEqual(2); - }); - it('should return exisitng index for the column for existing id', () => { - const indexById = new Map(); - indexById.set('Biomarker1', 4); +const summary1: CellSummary = { + biomarker_type: geneBiomarker, + cell_source: source1.id, + summary: [ + { + cell_id: cell1, + cell_label: cell1, + count: 75, + percentage: 0.75, + genes: [ + { + gene_id: gene1, + gene_label: gene1, + ensemble_id: ensembleId1, + mean_expression: 0.2, + }, + ], + }, + ], +}; - const existingIndex = getColumnIndex(indexById, 'Biomarker1'); - expect(existingIndex).toEqual(4); - }); - }); +const summary2: CellSummary = { + biomarker_type: geneBiomarker, + cell_source: source2.id, + summary: [ + { + cell_id: cell2, + cell_label: cell2, + count: 25, + percentage: 0.25, + genes: [ + { + gene_id: gene1, + gene_label: gene1, + ensemble_id: ensembleId1, + mean_expression: 0.6, + }, + ], + }, + ], +}; - describe('getRow', () => { - it('should return new index for the row for non-exisiting id', () => { - const rowById = new Map(); - const newRowIndex = getRow(rowById, ''); +const summaries = [summary1, summary2]; - expect(newRowIndex).toEqual(['', 0]); +describe('Cell Summary Helpers', () => { + describe('filterSummaries(summaries, sources)', () => { + it('returns summaries with the specified sources', () => { + const result = filterSummaries(summaries, [source1]); + expect(result).toEqual([summary1]); }); }); - describe('expandRow', () => { - const mockRow = ['label', 999] as CellSummaryAggregateRow; - it('expands a row', () => { - expandRow(mockRow, 4); - expect(mockRow).toEqual(['label', 999, undefined, undefined]); + describe('combineSummariesByBiomarkerType(summaries, types)', () => { + it('combines summaries for each biomarker type', () => { + const result = combineSummariesByBiomarkerType(summaries, [geneBiomarker]); + expect(result).toEqual([ + { + cell_source: `Aggregated by ${geneBiomarker}`, + biomarker_type: geneBiomarker, + summary: [...summary1.summary, ...summary2.summary], + }, + ]); }); }); - describe('computeAggregate', () => { - it('should compute aggregate data correctly', () => { - const result: CellSummaryAggregate = computeAggregate(mockSummary); - expect(result.label).toEqual('Summary Label'); - }); - }); + describe('computeAggregate(summary)', () => { + it('creates aggregate data for display in a table', () => { + const [summary] = combineSummariesByBiomarkerType(summaries, [geneBiomarker]); + const { rows, columns } = computeAggregate(summary); + expect(columns).toEqual([`${gene1} [${ensembleId1}]`]); + expect(rows.length).toBe(2); - describe('mergeCellSummaries', () => { - const mergedResult = { - biomarkers: [ - { id: 'biomarker1', label: 'Biomarker 1' }, - { id: 'biomarker2', label: 'Biomarker 2' }, - { id: 'biomarker1', label: 'Biomarker 1' }, - { id: 'biomarker2', label: 'Biomarker 2' }, - ], - cellSource: '', - cells: [ - { id: 'cell1', label: 'Cell 1' }, - { id: 'cell2', label: 'Cell 2' }, - { id: 'cell1', label: 'Cell 1' }, - { id: 'cell2', label: 'Cell 2' }, - ], - label: 'Summary Label', - summaries: [ - { biomarker: 'Biomarker1', cell: 'cell1', count: 20, dataset_count: 2, meanExpression: 0.4, percentage: 50 }, - { biomarker: 'Biomarker2', cell: 'cell1', count: 5, dataset_count: 1, meanExpression: 0.5, percentage: 30 }, - ], - }; - it('should merge cell summaries', () => { - const mergedSummaries = mergeCellSummaries([mockSummary, mockSummary2, mockSummary3], 'Summary Label'); - expect(mergedSummaries).toStrictEqual(mergedResult); + const [row1] = rows; + const [entry] = summary1.summary; + const [gene] = entry.genes; + expect(row1).toEqual([ + cell1, + entry.count, + { + color: gene.mean_expression, + size: entry.percentage, + data: { + cell: entry.cell_id, + biomarker: gene.gene_id, + count: entry.count, + meanExpression: gene.mean_expression, + percentage: entry.percentage, + dataset_count: 1, + }, + }, + ]); }); }); }); diff --git a/libs/state/src/lib/cell-summary/cell-summary.helpers.ts b/libs/state/src/lib/cell-summary/cell-summary.helpers.ts index 8568f3670..9d5d33f34 100644 --- a/libs/state/src/lib/cell-summary/cell-summary.helpers.ts +++ b/libs/state/src/lib/cell-summary/cell-summary.helpers.ts @@ -1,171 +1,227 @@ -import { Biomarker, Cell, CellSummary, CellSummaryRow, SourceReference } from '@hra-ui/services'; +import { CellSummary, Iri, SourceReference } from '@hra-ui/services'; +import { CellSummaryAggregate, CellSummaryAggregateCell, CellSummaryAggregateRow } from './cell-summary.model'; -import { - BIOMARKER_TYPES, - CellSummaryAggregate, - CellSummaryAggregateCell, - CellSummaryAggregateRow, -} from './cell-summary.model'; +/** Capitalizes the first character */ +function capitalize(str: string): string { + return str.slice(0, 1).toUpperCase() + str.slice(1); +} /** - * This function gets the index of the column if it does not have any + * Returns summaries with ids that are included in a source reference array */ -export function getColumnIndex(indexById: Map, id: string): number { - if (!indexById.has(id)) { - indexById.set(id, indexById.size + 2); - } - - return indexById.get(id) as number; +export function filterSummaries(summaries: CellSummary[], sources: SourceReference[]): CellSummary[] { + const sourceIds = new Set(sources.map((source) => source.id)); + return summaries.filter((summary) => sourceIds.has(summary.cell_source)); } /** - * This function gets the index of the row if it does not have any + * Combines multiple summaries into one per biomarker type + * + * @param summaries Summaries + * @param types Biomarker types + * @returns One summary for each biomarker type */ -export function getRow(rowById: Map, id: string): CellSummaryAggregateRow { - if (!rowById.has(id)) { - rowById.set(id, ['', 0]); +export function combineSummariesByBiomarkerType(summaries: CellSummary[], types: string[]): CellSummary[] { + const summariesByBiomarkerType: Record = {}; + for (const summary of summaries) { + summariesByBiomarkerType[summary.biomarker_type] ??= []; + summariesByBiomarkerType[summary.biomarker_type].push(summary); } - return rowById.get(id) as CellSummaryAggregateRow; + const results: CellSummary[] = []; + for (const type of types) { + const summaries = summariesByBiomarkerType[type] ?? []; + const items = summaries.reduce((acc, { summary }) => acc.concat(summary), []); + results.push({ + cell_source: `Aggregated by ${type}` as Iri, + biomarker_type: type, + summary: items, + }); + } + + return results; } -/** - * This function expands the current row length with undefined if its length is - * less than the given length - */ -export function expandRow(row: CellSummaryAggregateRow, length: number): void { - if (row.length < length) { - const fillStart = row.length; - row.length = length; - row.fill(undefined, fillStart); +/** Column object */ +type SummaryColumnObj = CellSummary['summary'][number]['genes'][number]; +/** Row object */ +type SummaryRowObj = CellSummary['summary'][number]; + +/** Helper class for building aggregate summaries */ +export class AggregateBuilder { + /** Mapping from column id to index */ + private readonly columnIndex = new Map(); + /** Mapping from row id to index */ + private readonly rowIndex = new Map(); + /** Column labels */ + private readonly columns: string[] = []; + /** Aggregate rows */ + private readonly rows: CellSummaryAggregateRow[] = []; + + /** + * Update the cell count for a row + * @param rowObj Raw row object + */ + updateRowCount(rowObj: SummaryRowObj): void { + const row = this.getRow(rowObj); + (row[1] as number) += rowObj.count; } -} -/** - * This function retrieves the label of an item from an array based on its ID, - * and if not found, it returns a default string indicating the absence of a label for the specified item type. - */ -export function getLabel(items: T[], id: string, what: string): string { - return items.find((item) => item.id === id)?.label ?? ``; -} + /** + * Update the entry corresponding to the row/column objects + * + * @param rowObj Raw row object + * @param columnObj Raw column object + */ + updateEntry(rowObj: SummaryRowObj, columnObj: SummaryColumnObj): void { + const row = this.getRow(rowObj); + const index = this.getColumnIndex(columnObj); + row[index] ??= { + color: 0, + size: 0, + data: { + cell: rowObj.cell_id, + biomarker: columnObj.gene_id, + count: 0, + meanExpression: 0, + percentage: 0, + dataset_count: 0, + }, + }; -/** - * This function calculates and returns the total count by iterating over a row array and summing - * up the count property of each object entry, while ignoring non-object entries, with an initial value of 0. - */ -// export function getTotalCount(row: CellSummaryAggregateRow): number { -// return row.reduce((acc, entry) => acc + (typeof entry === 'object' ? entry.data.count : 0), 0); -// } + const entry = row[index] as CellSummaryAggregateCell; -/** - * The computeAggregate function takes a summary object and performs aggregation operations on its properties - */ -export function computeAggregate(summary: CellSummary): CellSummaryAggregate { - const { label, cells, biomarkers, summaries } = summary; - const columnIndexByBiomarker = new Map(); - const rowsByCell = new Map(); + // Update count + entry.data.count += rowObj.count; - for (const summary of summaries) { - const { biomarker, cell } = summary; - const columnIndex = getColumnIndex(columnIndexByBiomarker, biomarker); - const row = getRow(rowsByCell, cell); - - expandRow(row, columnIndex); - row[columnIndex] = { - color: summary.meanExpression, - size: summary.percentage, - data: summary, - }; + // Update meanExpression + const { dataset_count: count = 0, meanExpression } = entry.data; + const cumulativeMeanExpression = (count * meanExpression + columnObj.mean_expression) / (count + 1); + entry.data.dataset_count = count + 1; + entry.data.meanExpression = cumulativeMeanExpression; + entry.color = cumulativeMeanExpression; } - for (const [cell, row] of rowsByCell.entries()) { - row[0] = getLabel(cells, cell, 'cell'); - row[1] = row.find((item): item is CellSummaryAggregateCell => typeof item === 'object')?.data.count; + /** + * Runs the final computations and returns the result + * + * @returns The finalized rows and columns + */ + finalize(): { columns: string[]; rows: CellSummaryAggregateRow[] } { + const { columns, order } = this.sortColumns(); + const rows = this.sortRows(order); + const totalCount = rows.reduce((acc, row) => acc + (row[1] ?? 0), 0); + for (const entry of this.entries()) { + const percentage = entry.data.count / totalCount; + entry.size = entry.data.percentage = percentage; + } + + return { columns, rows }; } - const columns = Array.from(columnIndexByBiomarker.keys()).map((id) => getLabel(biomarkers, id, 'biomarker')); - return { label, columns, rows: Array.from(rowsByCell.values()) }; -} + /** + * Get the index for a column object. Adds a new column if necessary. + * + * @param obj Raw column object + * @returns The associated index + */ + private getColumnIndex(obj: SummaryColumnObj): number { + const { columnIndex, columns } = this; + const id = `${obj.gene_id}/${obj.ensemble_id}`; + let index = columnIndex.get(id); + if (index === undefined) { + index = columnIndex.size + 2; + columnIndex.set(id, index); + columns.push(`${obj.gene_label} [${obj.ensemble_id}]`); + } -/** - * Returns summaries with ids that are included in a source reference array - */ -export function filterSummaries(summaries: CellSummary[], sources: SourceReference[]): CellSummary[] { - const sourceIds = new Set(sources.map((source) => source.id)); - return summaries.filter((summary) => sourceIds.has(summary.cellSource)); -} + return index; + } -/** - * Returns the number of times an id shows up in countsList array - */ -export function calculateDatasetCount(id: string, countsList: Record[]): number { - let result = 0; - for (const list of countsList) { - if (list[id]) { - result = result + 1; + /** + * Gets the row for a row object. Adds a new row if necessary. + * + * @param obj Raw row object + * @returns The associated row + */ + private getRow(obj: SummaryRowObj): CellSummaryAggregateRow { + const { rowIndex, rows } = this; + let index = rowIndex.get(obj.cell_id); + if (index === undefined) { + index = rows.length; + rowIndex.set(obj.cell_id, index); + rows.push([obj.cell_label, 0]); } + + return rows[index]; } - return result; -} -/** - * Merges summaries of each biomarker type and returns an array of summaries - */ -export function combineSummaries(summaries: CellSummary[]): CellSummary[] { - return BIOMARKER_TYPES.map((type) => mergeCellSummaries(summaries, type)); -} + /** + * Sorts the columns + * + * @returns The sorted columns and an array with the new index order + */ + private sortColumns(): { columns: string[]; order: number[] } { + const { columns } = this; + const indexedColumns = columns.map((col, index) => [index + 2, col] as const); + indexedColumns.sort((a, b) => (a[1] <= b[1] ? -1 : 1)); + return { + columns: indexedColumns.map((item) => item[1]), + order: indexedColumns.map((item) => item[0]), + }; + } -/** - * Merges cell summaries together into one cell summary - * Calculates total dataset counts and mean expressions for summaries - */ -export function mergeCellSummaries(summaries: CellSummary[], label: string): CellSummary { - const filteredSummaries = summaries.filter((summary) => summary.label === label); - const aggregateBiomarkers: Biomarker[] = []; - const aggregateCells: Cell[] = []; - const aggregateSummaries: CellSummaryRow[] = []; - const summariesList: CellSummaryRow[][] = []; - - for (const summary of filteredSummaries) { - for (const biomarker of summary.biomarkers) { - aggregateBiomarkers.push(biomarker); - } - for (const cell of summary.cells) { - aggregateCells.push(cell); - } + /** + * Sorts the row data + * + * @param order The new column order + * @returns Rows with data sorted in the new column order + */ + private sortRows(order: number[]): CellSummaryAggregateRow[] { + return this.rows.map((row) => { + const sorted: CellSummaryAggregateRow = [row[0], row[1]]; + for (let index = 2; index < row.length; index++) { + if (row[index] !== undefined) { + const newIndex = order[index - 2]; + sorted[newIndex] = row[index]; + } + } + + return sorted; + }); + } - summariesList.push(summary.summaries); - const countsList: Record[] = []; - for (const summaries of summariesList) { - const datasetCounts: Record = {}; - for (const sum of summaries) { - const id = sum.cell + sum.biomarker; - if (!datasetCounts[id]) { - datasetCounts[id] = true; + /** + * Iterates of all defined aggregate entries + */ + private *entries(): Generator { + for (const row of this.rows) { + for (let index = 2; index < row.length; index++) { + if (row[index] !== undefined) { + yield row[index] as CellSummaryAggregateCell; } } - countsList.push(datasetCounts); } + } +} - for (const sum of summary.summaries) { - const match = aggregateSummaries.find((entry) => entry.biomarker === sum.biomarker && entry.cell === sum.cell); - if (match) { - const id = match.cell + match.biomarker; - match.count += sum.count; - match.meanExpression = - (match.count * match.meanExpression + sum.count * sum.meanExpression) / (match.count + sum.count); - match.dataset_count = calculateDatasetCount(id, countsList); - } else { - aggregateSummaries.push({ ...sum, dataset_count: 1 }); - } +/** + * Aggregates cell summaries for display in a table + * + * @param summary Raw cell summaries + * @returns Aggregated cell summary rows + */ +export function computeAggregate(summary: CellSummary): CellSummaryAggregate { + const state = new AggregateBuilder(); + for (const cell of summary.summary) { + state.updateRowCount(cell); + for (const gene of cell.genes) { + state.updateEntry(cell, gene); } } return { - label: label, - biomarkers: aggregateBiomarkers, - cells: aggregateCells, - summaries: aggregateSummaries, - cellSource: '', + label: `${capitalize(summary.biomarker_type)} Biomarkers`, + ...state.finalize(), }; } diff --git a/libs/state/src/lib/cell-summary/cell-summary.model.ts b/libs/state/src/lib/cell-summary/cell-summary.model.ts index 3ad6d93ba..6257a4793 100644 --- a/libs/state/src/lib/cell-summary/cell-summary.model.ts +++ b/libs/state/src/lib/cell-summary/cell-summary.model.ts @@ -35,7 +35,7 @@ export interface CellSummaryModel { export type Context = StateContext; /** Biomarker type labels */ -export const BIOMARKER_TYPES = ['Gene Biomarkers', 'Protein Biomarkers', 'Lipid Biomarkers']; +export const BIOMARKER_TYPES = ['gene', 'protein', 'lipid']; /** * The AGGREGATE_CELL is an object that contains the color, size and diff --git a/libs/state/src/lib/cell-summary/cell-summary.state.spec.ts b/libs/state/src/lib/cell-summary/cell-summary.state.spec.ts index 75c0c1958..3cff7f7ae 100644 --- a/libs/state/src/lib/cell-summary/cell-summary.state.spec.ts +++ b/libs/state/src/lib/cell-summary/cell-summary.state.spec.ts @@ -7,7 +7,7 @@ import { of } from 'rxjs'; import { ActiveFtuSelectors } from '../active-ftu'; import { FilterSummaries, Load } from './cell-summary.actions'; -import { CellSummaryModel } from './cell-summary.model'; +import { BIOMARKER_TYPES, CellSummaryModel } from './cell-summary.model'; import { CellSummaryState } from './cell-summary.state'; import { filterSummaries } from './cell-summary.helpers'; @@ -63,7 +63,7 @@ describe('CellSummaryState', () => { ctx.getState.mockReturnValue({ summaries: [], aggregates: [], - biomarkerTypes: [], + biomarkerTypes: BIOMARKER_TYPES, filteredSummaries: [], summariesByBiomarker: [], }); @@ -104,9 +104,9 @@ describe('CellSummaryState', () => { state.combineSummariesByBiomarker(ctx); expect(ctx.patchState).toHaveBeenCalledWith({ summariesByBiomarker: [ - { biomarkers: [], cellSource: '', cells: [], label: 'Gene Biomarkers', summaries: [] }, - { biomarkers: [], cellSource: '', cells: [], label: 'Protein Biomarkers', summaries: [] }, - { biomarkers: [], cellSource: '', cells: [], label: 'Lipid Biomarkers', summaries: [] }, + { biomarker_type: 'gene', cell_source: 'Aggregated by gene', summary: [] }, + { biomarker_type: 'protein', cell_source: 'Aggregated by protein', summary: [] }, + { biomarker_type: 'lipid', cell_source: 'Aggregated by lipid', summary: [] }, ], }); }); diff --git a/libs/state/src/lib/cell-summary/cell-summary.state.ts b/libs/state/src/lib/cell-summary/cell-summary.state.ts index d978476a5..5458b6762 100644 --- a/libs/state/src/lib/cell-summary/cell-summary.state.ts +++ b/libs/state/src/lib/cell-summary/cell-summary.state.ts @@ -4,7 +4,7 @@ import { Action, State } from '@ngxs/store'; import { Observable, switchMap, tap } from 'rxjs'; import { SourceRefsActions } from '../source-refs'; import { CombineSummariesByBiomarker, ComputeAggregates, FilterSummaries, Load, Reset } from './cell-summary.actions'; -import { combineSummaries, computeAggregate, filterSummaries } from './cell-summary.helpers'; +import { combineSummariesByBiomarkerType, computeAggregate, filterSummaries } from './cell-summary.helpers'; import { BIOMARKER_TYPES, CellSummaryModel, Context } from './cell-summary.model'; /** State handling cell summary data */ @@ -57,8 +57,8 @@ export class CellSummaryState { */ @Action(CombineSummariesByBiomarker) combineSummariesByBiomarker({ getState, patchState, dispatch }: Context): Observable { - const { filteredSummaries: summaries } = getState(); - const summariesByBiomarker = combineSummaries(summaries); + const { filteredSummaries, biomarkerTypes } = getState(); + const summariesByBiomarker = combineSummariesByBiomarkerType(filteredSummaries, biomarkerTypes); patchState({ summariesByBiomarker }); return dispatch(new ComputeAggregates());