diff --git a/angular.json b/angular.json index 2969a96d..d25d99fa 100644 --- a/angular.json +++ b/angular.json @@ -101,5 +101,8 @@ } } } + }, + "cli": { + "analytics": false } } diff --git a/package-lock.json b/package-lock.json index 88c5eb75..2368093c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "graphology-layout-force": "^0.2.4", "lodash": "^4.17.21", "ngx-bootstrap": "^19.0.1", + "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", "sigma": "^3.0.0-beta.38", "tslib": "^2.3.0", @@ -10597,9 +10598,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -10680,6 +10681,16 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/ngx-dropzone": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ngx-dropzone/-/ngx-dropzone-3.1.0.tgz", + "integrity": "sha512-5RBaEl07QUcY6sv/BBPyIxN6nbWY/KqTGheEKgbuGS0N1QPFY7NJUo8+X3fYUwQgLS+wjJeqPiR37dd0YNDtWA==", + "deprecated": "This package is deprecated and will no longer receive any updates. Please take a look at the official successor repo at hackingharold/ngx-dropzone", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", diff --git a/package.json b/package.json index daa80e4a..10a3ee03 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "graphology-layout-force": "^0.2.4", "lodash": "^4.17.21", "ngx-bootstrap": "^19.0.1", + "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", "sigma": "^3.0.0-beta.38", "tslib": "^2.3.0", diff --git a/public/chip-intelligence-processor-svgrepo-com.png b/public/chip-intelligence-processor-svgrepo-com.png new file mode 100644 index 00000000..d2e9cb1d Binary files /dev/null and b/public/chip-intelligence-processor-svgrepo-com.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 57614f9c..4d50c9e8 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon_old.ico b/public/favicon_old.ico new file mode 100644 index 00000000..57614f9c Binary files /dev/null and b/public/favicon_old.ico differ diff --git a/public/magnifying_glass.png b/public/magnifying_glass.png new file mode 100644 index 00000000..9583588d Binary files /dev/null and b/public/magnifying_glass.png differ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0642f024..a88efe17 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,7 +13,8 @@ import {VersionsService} from "./services/versions.service"; styleUrl: './app.component.scss' }) export class AppComponent { - subpages = ['Browse', 'Genes', 'Documentation', 'Download']; + title = 'SPONGE-web-frontend'; + subpages = ['Browse', 'Genes', 'SpongEffects', 'Documentation', 'Download']; version: WritableSignal; constructor(versionsService: VersionsService) { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 7c0a2ce7..7c3f93bf 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -10,7 +10,10 @@ import { } from "./routes/documentation/browse-functionalities/browse-functionalities.component"; import {BrowseSidebarComponent} from "./routes/documentation/browse-sidebar/browse-sidebar.component"; import {MoreComponent} from "./routes/documentation/more/more.component"; +import {SpongEffectsComponent} from "./routes/spongeffects/spongeffects.component"; import {GenesComponent} from "./routes/genes/genes.component"; +import {ExploreComponent} from "./routes/spongeffects/explore/explore.component"; +import {PredictComponent} from "./routes/spongeffects/predict/predict.component"; export const routes: Routes = [ { @@ -59,6 +62,20 @@ export const routes: Routes = [ path: 'download', component: DownloadComponent }, + { + path: 'spongeffects', + component: SpongEffectsComponent, + children: [ + { + path: '', + component: ExploreComponent + }, + { + path: 'predict', + component: PredictComponent + } + ], + }, { path: '**', redirectTo: '' diff --git a/src/app/components/info/info.component.html b/src/app/components/info/info.component.html new file mode 100644 index 00000000..20579fc8 --- /dev/null +++ b/src/app/components/info/info.component.html @@ -0,0 +1,37 @@ +@if (type() == 'modal') { +
+ +
+} @else { + + + + info + {{ title() }} + + @if (subtitle(); as subtitle) { + + {{ subtitle }} + + } + + + +} + + +

{{ title() }}

+ @if (subtitle(); as subtitle) { +

{{ subtitle }}

+ } + + + +
+ + + + diff --git a/src/app/components/info/info.component.scss b/src/app/components/info/info.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/info/info.component.spec.ts b/src/app/components/info/info.component.spec.ts new file mode 100644 index 00000000..6f7e9e07 --- /dev/null +++ b/src/app/components/info/info.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InfoComponent } from './info.component'; + +describe('InfoComponent', () => { + let component: InfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InfoComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/info/info.component.ts b/src/app/components/info/info.component.ts new file mode 100644 index 00000000..f0e8351f --- /dev/null +++ b/src/app/components/info/info.component.ts @@ -0,0 +1,31 @@ +import {Component, inject, input, viewChild} from '@angular/core'; +import {MatButton} from "@angular/material/button"; +import {MatExpansionModule} from "@angular/material/expansion"; +import {MatIcon} from "@angular/material/icon"; +import {MatDialog, MatDialogModule} from "@angular/material/dialog"; +import {NgTemplateOutlet} from "@angular/common"; + +@Component({ + selector: 'app-info', + imports: [ + MatButton, + MatExpansionModule, + MatIcon, + MatDialogModule, + NgTemplateOutlet + ], + templateUrl: './info.component.html', + styleUrl: './info.component.scss' +}) +export class InfoComponent { + dialog = inject(MatDialog); + dialogTemplate = viewChild('dialog'); + + title = input('What does this mean?'); + subtitle = input(); + type = input<'modal' | 'panel'>('modal'); + + openDialog() { + this.dialog.open(this.dialogTemplate()) + } +} diff --git a/src/app/interfaces.ts b/src/app/interfaces.ts index fedf47a6..3308ce2d 100644 --- a/src/app/interfaces.ts +++ b/src/app/interfaces.ts @@ -1,10 +1,10 @@ export interface Dataset { - "data_origin": string, - "dataset_ID": number, - "disease_name": string, + data_origin: string, + dataset_ID: number, + disease_name: string, disease_subtype: string | null, - "disease_type": string, - "download_url": string, + disease_type: string, + download_url: string, sponge_db_version: number } @@ -38,6 +38,18 @@ export interface RunInfo { "variance_cutoff": string } +export interface DatasetInfo { + dataset_ID: number, + disease_name: string, + data_origin: string, + disease_type: string, + download_url: string, + disease_subtype: string, + study_abbreviation: string, + version: number, + number_of_samples: number +} + export interface OverallCounts { count_interactions: number, count_interactions_sign: number, @@ -112,6 +124,57 @@ export interface BrowseQuery { minMScore: number } +export interface CeRNA { + betweenness: number, + eigenvector: number, + gene: Gene, + node_degree: number + run: { + dataset: { + data_origin: string, + dataset_ID: number, + disease_name: string + }, + run_ID: number + } +} + +export interface CeRNAInteraction { + "correlation": number, + "gene1": Gene, + "gene2": Gene, + "mscor": number, + "p_value": number, + "run": { + "dataset": { + "data_origin": string, + "dataset_ID": number, + "disease_name": string + }, + "run_ID": number + } +} + +export interface CeRNAQuery { + disease: Dataset, + geneSorting: GeneSorting, + maxGenes: number, + minDegree: number, + minBetweenness: number, + minEigen: number, + interactionSorting: InteractionSorting, + maxInteractions: number, + maxPValue: number, + minMScore: number +} + +export interface CeRNAExpression { + "dataset": string, + "expr_value": number, + "gene": Gene, + "sample_ID": string +} + export interface GeneExpression { "dataset": string, "expr_value": number, @@ -188,6 +251,178 @@ export interface WikiPathway { wp_key: string } +// from spongEffects +// route responses + +export interface SpongEffectsRun { + spongeEffects_run_ID: number, + m_scor_threshold: number, + p_adjust_threshold: number, + modules_cutoff: number, + bin_size: number, + min_size: number, + max_size: number, + min_expr: number, + method: string, + cv_folds: number + level: string, + sponge_run_ID: number, + m_max: number, + log_level: string, + sponge_db_version: string, + dataset_ID: number, + disease_name: string, + data_origin: string, + disease_type: string, + download_url: string, + disease_subtype: string, +} + +export interface RunPerformance { + model_type: string, + split_type: string, + accuracy: number, + kappa: number, + accuracy_lower: number, + accuracy_upper: number, + accuracy_null: number, + accuracy_p_value: number, + mcnemar_p_value: number +} + +export interface RunClassPerformance { + prediction_class: string; + sensitivity: number; + specificity: number; + pos_pred_value: number; + neg_pred_value: number; + precision_value: number; + recall: number; + f1: number; + prevalence: number; + detection_rate: number; + detection_prevalence: number; + balanced_accuracy: number; + spongEffects_run: { + model_type: string; + split_type: string; + }; +} + +export interface EnrichmentScoreDistributions { + prediction_class: string; + enrichment_score: number; + density: number; +} + +export interface SpongEffectsGeneModules { + ensg_number: string; + gene_symbol: string; + mean_gini_decrease: number; + mean_accuracy_decrease: number; +} + +export interface SpongEffectsGeneModuleMembers { + hub_ensg_number: string; + hub_gene_symbol: string; + member_ensg_number: string; + member_gene_symbol: string; +} + +export interface SpongEffectsTranscriptModules { + enst_number: string; + gene: { + ensg_number: string; + gene_symbol: string; + }; + mean_gini_decrease: number; + mean_accuracy_decrease: number; +} + +export interface SpongEffectsTranscriptModuleMembers { + hub_enst_number: string; + hub_gene: { + ensg_number: string; + gene_symbol: string; + }; + member_enst_number: string; + member_gene: { + ensg_number: string; + gene_symbol: string; + }; +} + +export interface PredictCancerType { + meta: { + runtime: number; + level: string; + n_samples: number; + type_predict: string; + subtype_predict: string; + }; + data: { + sampleID: string; + typePrediction: string; + subtypePrediction: string; + }[]; +} + +export interface ExploreQuery { + selectedCancer: string, + selectedLevel: string +} + +// other interfaces for spongEffects + +export interface Metric { + name: string, + split: string + lower: number, + upper: number, + idx: number +} + +export interface SelectElement { + value: string, + viewValue: string +} + +export interface CancerInfo { + text: string[], + link: string; +} + +export interface PlotData { + x: number[], + y: number[] +} + +export interface PlotlyData { + data: any, + layout?: any, + config?: any +} + +export interface Tab extends SelectElement { + icon: string +} + +export interface LinearRegression { + slope: number, + x0: number +} + +export interface ExampleExpression { + id: string; + sample1: number; + sample2: number; + sample3: number; + sample4: number; + sampleN: number; +} + + + export interface AlternativeSplicingEvent { event_name: string, event_type: string, diff --git a/src/app/routes/browse/survival-analysis/kmplot/kmplot.component.html b/src/app/routes/browse/survival-analysis/kmplot/kmplot.component.html index 094b2102..d7f2068f 100644 --- a/src/app/routes/browse/survival-analysis/kmplot/kmplot.component.html +++ b/src/app/routes/browse/survival-analysis/kmplot/kmplot.component.html @@ -16,6 +16,6 @@ } } -
+
diff --git a/src/app/routes/spongeffects/explore/explore.component.html b/src/app/routes/spongeffects/explore/explore.component.html new file mode 100644 index 00000000..c38d6d6b --- /dev/null +++ b/src/app/routes/spongeffects/explore/explore.component.html @@ -0,0 +1,35 @@ + + + + + + + + + +

The spongEffects enrichment score distributions can give an insight into the model capability to + differentiate between the given classes.

+
+
+ +
+
+ + +

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of + nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, + it means the variable is more important in the model.

+

Module centralities that are at the top right of the plot below play a crucial role in distinguishing + between the various cancer (sub-)types.

+
+
+ + +

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological + functions are enriched in gene expression data.

+

The information from GSEA can help find biological differences between disease (sub-)types

+
+
+
+
+
diff --git a/src/app/routes/spongeffects/explore/explore.component.scss b/src/app/routes/spongeffects/explore/explore.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/explore.component.spec.ts b/src/app/routes/spongeffects/explore/explore.component.spec.ts new file mode 100644 index 00000000..8eecc812 --- /dev/null +++ b/src/app/routes/spongeffects/explore/explore.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExploreComponent } from './explore.component'; + +describe('ExploreComponent', () => { + let component: ExploreComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExploreComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ExploreComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts new file mode 100644 index 00000000..0d77dd42 --- /dev/null +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -0,0 +1,47 @@ +import {Component, signal} from '@angular/core'; +import {MatExpansionModule} from "@angular/material/expansion"; +import {MatIconModule} from "@angular/material/icon"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; +import {MatCardModule} from '@angular/material/card'; +import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; +import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; +import {MatTabsModule} from "@angular/material/tabs"; +import {fromEvent} from "rxjs"; +import {InfoComponent} from "../../../components/info/info.component"; + +@Component({ + selector: 'app-explore', + imports: [ + MatExpansionModule, + MatFormFieldModule, + FormsModule, + ReactiveFormsModule, + MatButtonToggleModule, + MatProgressSpinnerModule, + MatIconModule, + MatCardModule, + ClassPerformancePlotComponent, + OverallAccPlotComponent, + MatTabsModule, + InfoComponent, + + ], + templateUrl: './explore.component.html', + styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] +}) +export class ExploreComponent { + refreshSignal = signal(0); + + constructor() { + fromEvent(window, 'resize').subscribe(() => { + this.refresh(); + }); + } + + refresh() { + this.refreshSignal.update(v => v + 1); + } +} diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.html b/src/app/routes/spongeffects/explore/form/explore-form.component.html new file mode 100644 index 00000000..20d7172d --- /dev/null +++ b/src/app/routes/spongeffects/explore/form/explore-form.component.html @@ -0,0 +1,14 @@ + + Disease + + @for (disease of diseases$(); track disease) { + {{ disease }} + } + + +
+ + Gene + Transcript + +
diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.scss b/src/app/routes/spongeffects/explore/form/explore-form.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts b/src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts new file mode 100644 index 00000000..57b3b972 --- /dev/null +++ b/src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ExploreFormComponent} from './explore-form.component'; + +describe('FormComponent', () => { + let component: ExploreFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExploreFormComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ExploreFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.ts b/src/app/routes/spongeffects/explore/form/explore-form.component.ts new file mode 100644 index 00000000..3ce24cc7 --- /dev/null +++ b/src/app/routes/spongeffects/explore/form/explore-form.component.ts @@ -0,0 +1,25 @@ +import {Component, inject} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatSelectModule} from "@angular/material/select"; +import {MatButtonToggleModule} from "@angular/material/button-toggle"; +import {ExploreService} from "../service/explore.service"; + +@Component({ + selector: 'app-explore-form', + imports: [ + FormsModule, + MatFormFieldModule, + MatSelectModule, + ReactiveFormsModule, + MatButtonToggleModule + ], + templateUrl: './explore-form.component.html', + styleUrl: './explore-form.component.scss' +}) +export class ExploreFormComponent { + exploreService = inject(ExploreService) + level$ = this.exploreService.level$; + diseases$ = this.exploreService.diseaseNames$; + disease$ = this.exploreService.selectedDisease$; +} diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html new file mode 100644 index 00000000..7751963b --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html @@ -0,0 +1,14 @@ +
+ +
+ +
+
+
+ + +

This plot shows the performance of the individual class predictions of the model that was trained with the + spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in + the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions + should be made with more caution if this it not the case.

+
diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.scss b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.spec.ts b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.spec.ts new file mode 100644 index 00000000..41f2ceb2 --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClassPerformancePlotComponent } from './class-performance-plot.component'; + +describe('ClassPerformancePlotComponent', () => { + let component: ClassPerformancePlotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ClassPerformancePlotComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ClassPerformancePlotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts new file mode 100644 index 00000000..2261ebfc --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts @@ -0,0 +1,150 @@ +import {Component, computed, ElementRef, inject, Renderer2, resource, ResourceRef, ViewChild} from '@angular/core'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatIconModule} from '@angular/material/icon'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {PlotlyData, SelectElement,} from '../../../../../interfaces'; +import {VersionsService} from '../../../../../services/versions.service'; +import {BackendService} from '../../../../../services/backend.service'; +import {sum} from "lodash"; +import {ExploreService} from "../../service/explore.service"; +import {InfoComponent} from "../../../../../components/info/info.component"; + +declare var Plotly: any; + +@Component({ + selector: 'app-class-performance-plot', + imports: [ + MatExpansionModule, + MatIconModule, + MatFormFieldModule, + MatSelectModule, + FormsModule, + ReactiveFormsModule, + MatProgressBarModule, + InfoComponent + ], + templateUrl: './class-performance-plot.component.html', + styleUrl: './class-performance-plot.component.scss' +}) +export class ClassPerformancePlotComponent { + versionService = inject(VersionsService); + exploreService = inject(ExploreService); + backend = inject(BackendService); + + version$ = this.versionService.versionReadOnly; + level$ = this.exploreService.level$; + disease$ = this.exploreService.selectedDisease$; + + @ViewChild("classModelPerformancePlot") classPerformancePlotDiv!: ElementRef; + plotClassPerformance: ResourceRef; + + + performanceMeasures: SelectElement[] = [ + {value: 'balanced_accuracy', viewValue: "Balanced Accuracy"}, + {value: 'detection_prevalence', viewValue: "Detection Prevalence"}, + {value: 'detection_rate', viewValue: "Detection Rate"}, + {value: 'f1', viewValue: "F1"}, + {value: 'neg_pred_value', viewValue: "Negative Prediction Value"}, + {value: 'pos_pred_value', viewValue: "Positive Prediction Value"}, + {value: 'precision_value', viewValue: "Precision"}, + {value: 'prevalence', viewValue: "Prevalence"}, + {value: 'recall', viewValue: "Recall"}, + {value: 'sensitivity', viewValue: "Sensitivity"}, + {value: 'specificity', viewValue: "Specificity"} + ]; + performanceMeasure: SelectElement = this.performanceMeasures[0]; + performanceSelectPanelIsOpen: boolean = false; + includeModuleMembers: boolean = false; + classPerformanceLoading: boolean = true; + + + constructor(private renderer: Renderer2) { + // signals from explore service (explore params) + this.plotClassPerformance = resource({ + request: computed(() => { + return { + version: this.versionService.versionReadOnly()(), + cancer: this.exploreService.selectedDisease$(), + level: this.exploreService.level$() + } + }), + loader: async (param) => { + const version = param.request.version; + const gene = param.request.cancer; + const level = param.request.level; + if (version === undefined || gene === undefined || level === undefined) return; + return await this.plotModelClassPerformance(version, gene, level); + } + }); + } + + + async plotModelClassPerformance(version: number, cancer: string, level: string): Promise { + const performanceData = await this.backend.getRunClassPerformance(version, cancer, level); + + // group the data by model type + const traceGroups: { [key: string]: any[] } = {}; + performanceData.forEach(entry => { + const modelType = entry.spongEffects_run.model_type; + if (!traceGroups[modelType]) { + traceGroups[modelType] = []; + } + traceGroups[modelType].push(entry); + }); + // build actual traces + const traces = []; + for (const modelType in traceGroups) { + if (traceGroups.hasOwnProperty(modelType)) { + const group = traceGroups[modelType]; + + const trace = { + x: group.map(entry => entry.prediction_class), + y: group.map(entry => entry[this.performanceMeasure.value]), + type: 'bar', + name: modelType, + }; + + traces.push(trace); + } + } + const meanTextLength: number = Math.round(sum(traces[0].x.map(d => d.length)) / traces[0].x.length); + const textPad: number = meanTextLength * 10.5; + const containerWidth = this.renderer.selectRootElement(this.classPerformancePlotDiv.nativeElement).offsetWidth; + // const angle: number = meanTextLength > 15 ? 90: 0; + // angle 90 if number of bars is greater than 10 + const uniqueBars = new Set(traces.flatMap(trace => trace.x)).size; + const angle: number = uniqueBars > 10 ? -90 : 0; + const layout = { + barmode: 'group', + autosize: true, + // width: containerWidth, + xaxis: { + autosize: true, + tickangle: angle + }, + yaxis: { + title: this.performanceMeasure.viewValue + }, + margin: { + t: 8, + b: angle == -90 ? textPad : 40, + l: 70, + // r: 0 + }, + legend: { + orientation: "h", + x: 0.5, + y: 1.25 + }, + paper_bgcolor: 'rgba(0,0,0,0)', + plot_bgcolor: 'rgba(0,0,0,0)' + }; + const config = {responsive: true}; + const data = {data: traces, layout: layout, config: config}; + return Plotly.newPlot(this.classPerformancePlotDiv.nativeElement, data.data, data.layout, data.config); + + } +} diff --git a/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.html b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.html new file mode 100644 index 00000000..93e9c541 --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.html @@ -0,0 +1 @@ +

enrichment-class-plot works!

diff --git a/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.scss b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.spec.ts b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.spec.ts new file mode 100644 index 00000000..945cd91e --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EnrichmentClassPlotComponent } from './enrichment-class-plot.component'; + +describe('EnrichmentClassPlotComponent', () => { + let component: EnrichmentClassPlotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EnrichmentClassPlotComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EnrichmentClassPlotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.ts b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.ts new file mode 100644 index 00000000..fee3a28b --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/enrichment-class-plot/enrichment-class-plot.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-enrichment-class-plot', + imports: [], + templateUrl: './enrichment-class-plot.component.html', + styleUrl: './enrichment-class-plot.component.scss' +}) +export class EnrichmentClassPlotComponent { + +} diff --git a/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.html b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.html new file mode 100644 index 00000000..7a6c952d --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.html @@ -0,0 +1 @@ +

lollipop-plot works!

diff --git a/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.scss b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.spec.ts b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.spec.ts new file mode 100644 index 00000000..1f3d9c6f --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LollipopPlotComponent } from './lollipop-plot.component'; + +describe('LollipopPlotComponent', () => { + let component: LollipopPlotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LollipopPlotComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LollipopPlotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.ts b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.ts new file mode 100644 index 00000000..70e98ce3 --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/lollipop-plot/lollipop-plot.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-lollipop-plot', + imports: [], + templateUrl: './lollipop-plot.component.html', + styleUrl: './lollipop-plot.component.scss' +}) +export class LollipopPlotComponent { + +} diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html new file mode 100644 index 00000000..fdbe7a1c --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html @@ -0,0 +1,19 @@ +@if (plotOverallAccResource.isLoading()) { + +} @else { + @if (!plotOverallAccResource.value()) { +

No data available

+ } +} +
+
+
+ +

+ The models performance is compared against a model that was trained on randomly selected SPONGE centralities. + The model that was trained on the spongEffects modules should outperform the random approach. + Dotted lines represent the overall accuracy for the test split of the data and solid lines show analogously show + the training performance. + Each line start (circle) and end (diamond) mark the lower and upper balanced accuracy bounds of a specific model. +

+
diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.scss b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.spec.ts b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.spec.ts new file mode 100644 index 00000000..cd8a91f7 --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OverallAccPlotComponent } from './overall-acc-plot.component'; + +describe('OverallAccPlotComponent', () => { + let component: OverallAccPlotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OverallAccPlotComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OverallAccPlotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts new file mode 100644 index 00000000..348d1409 --- /dev/null +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts @@ -0,0 +1,210 @@ +import {Component, computed, effect, ElementRef, inject, input, resource, viewChild} from '@angular/core'; +import {Metric, PlotlyData, RunPerformance} from '../../../../../interfaces'; +import {BackendService} from '../../../../../services/backend.service'; +import {VersionsService} from '../../../../../services/versions.service'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatIconModule} from '@angular/material/icon'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {ExploreService} from "../../service/explore.service"; +import {InfoComponent} from "../../../../../components/info/info.component"; + +declare var Plotly: any; + +@Component({ + selector: 'app-overall-acc-plot', + imports: [ + MatExpansionModule, + MatIconModule, + MatFormFieldModule, + MatSelectModule, + FormsModule, + ReactiveFormsModule, + MatProgressBarModule, + InfoComponent + ], + templateUrl: './overall-acc-plot.component.html', + styleUrl: './overall-acc-plot.component.scss' +}) +export class OverallAccPlotComponent { + versionService = inject(VersionsService); + exploreService = inject(ExploreService); + backend = inject(BackendService); + refreshSignal$ = input(); + + overallAccPlot = viewChild.required>('overallAccuracyPlot'); + + // plot parameters + defaultPlotMode: string = "lines+markers"; + defaultLineWidth: number = 4; + defaultMarkerSize: number = 10; + + plotOverallAccResource = resource({ + request: computed(() => { + return { + version: this.versionService.versionReadOnly()(), + cancer: this.exploreService.selectedDisease$(), + level: this.exploreService.level$() + } + }), + loader: async (param) => { + const version = param.request.version; + const gene = param.request.cancer; + const level = param.request.level; + if (version === undefined || gene === undefined || level === undefined) return; + const data = this.getOverallAccuracyData(version, gene, level); + return await this.plotOverallAccuracyPlot(data); + } + }); + + constructor() { + effect(() => { + this.refreshSignal$(); + this.refreshPlot(); + }); + } + + + async getOverallAccuracyData(version: number, cancer: string, level: string): Promise { + const modelPerformances = await this.backend.getRunPerformance(version, cancer, level); + return modelPerformances.map((entry: RunPerformance, idx: number): Metric => { + return { + name: entry.model_type, + split: entry.split_type, + lower: entry.accuracy_lower, + upper: entry.accuracy_upper, + idx: idx + 1 + } + }); + }; + + async plotOverallAccuracyPlot(metricData: Promise): Promise { + // set main layout options + const layout = { + autosize: true, + yaxis: { + showline: false, + showticklabels: true, + tickvals: [] as number[], + ticktext: [] as string[] + }, + margin: { + t: 40, + // b: 40, + // l: 0, + // r: 200, + }, + annotations: [ + // x-axis label + { + xref: "paper", + yref: "paper", + x: 0.5, + y: -0.1, + xanchor: "center", + yanchor: "top", + text: "Overall model accuracy", + showarrow: false + } + ], + legend: { + traceorder: "reversed", + x: 1, // Position legend to the right + xanchor: 'left', + y: 1, + yanchor: 'top' + }, + paper_bgcolor: 'rgba(0,0,0,0)', + plot_bgcolor: 'rgba(0,0,0,0)' + }; + const metrics: Metric[] = await metricData; + const data = metrics.map(metric => { + const col: string = metric.name == "modules" ? "green" : "orange" + // Add model name to y-axis labels + layout.yaxis.tickvals.push(metric.idx + 1); + layout.yaxis.ticktext.push(`Model ${metric.idx}`); + // data points + return { + x: [metric.lower, metric.upper], + y: [metric.idx + 1, metric.idx + 1], + mode: this.defaultPlotMode, + name: metric.name + " (" + metric.split + ")", + text: ["Lower Bound (Accuracy)", "Upper Bound (Accuracy)"], + hovertemplate: "%{text}: %{x:.2f}", + line: { + width: this.defaultLineWidth, + color: col, + dash: metric.split == "train" ? "solid" : "dash" + }, + marker: { + size: this.defaultMarkerSize, + symbol: ['circle', 'diamond'], + color: col + }, + showlegend: false + } + }); + + + // Add custom legend entries + const customLegend = [ + { + x: [null], + y: [null], + mode: 'lines', + name: 'Modules (Train)', + line: { + color: 'green', + dash: 'solid' + }, + type: 'scatter' + }, + { + x: [null], + y: [null], + mode: 'lines', + name: 'Modules (Test)', + line: { + color: 'green', + dash: 'dash' + }, + type: 'scatter' + }, + { + x: [null], + y: [null], + mode: 'lines', + name: 'Random (Train)', + line: { + color: 'orange', + dash: 'solid' + }, + type: 'scatter' + }, + { + x: [null], + y: [null], + mode: 'lines', + name: 'Random (Test)', + line: { + color: 'orange', + dash: 'dash' + }, + type: 'scatter' + } + ]; + + const config = {responsive: true}; + // remove loading spinner and show plot + return Plotly.newPlot(this.overallAccPlot().nativeElement, [...data, ...customLegend], layout, config); + } + + refreshPlot() { + const plotDiv = this.overallAccPlot().nativeElement; + if (plotDiv.checkVisibility()) { + Plotly.Plots.resize(plotDiv); + } + } +} diff --git a/src/app/routes/spongeffects/explore/service/explore.service.spec.ts b/src/app/routes/spongeffects/explore/service/explore.service.spec.ts new file mode 100644 index 00000000..93bc70e5 --- /dev/null +++ b/src/app/routes/spongeffects/explore/service/explore.service.spec.ts @@ -0,0 +1,16 @@ +import {TestBed} from '@angular/core/testing'; + +import {ExploreService} from './explore.service'; + +describe('ExploreServiceService', () => { + let service: ExploreService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ExploreService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/service/explore.service.ts b/src/app/routes/spongeffects/explore/service/explore.service.ts new file mode 100644 index 00000000..8788920e --- /dev/null +++ b/src/app/routes/spongeffects/explore/service/explore.service.ts @@ -0,0 +1,49 @@ +import {inject, Injectable, linkedSignal, signal} from '@angular/core'; +import {SpongEffectsService} from "../../../../services/spong-effects.service"; +import {sum} from "lodash"; + +export class Cancer { + value: string; + viewValue: string; + allSubTypes: string[]; + sampleSizes: number[]; + + base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; + + constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[],) { + this.value = value; + this.viewValue = viewValue; + this.allSubTypes = allSubTypes; + this.sampleSizes = sampleSizes; + } + + addSubtype(subtype: string) { + this.allSubTypes.push(subtype); + } + + addSampleSize(sampleSize: number) { + if (sampleSize != null) this.sampleSizes.push(sampleSize); + } + + totalNumberOfSamples() { + return sum(this.sampleSizes.filter(s => s >= 0)); + } + + toString() { + return this.viewValue + " - (" + this.value + ")"; + } + + getUrl() { + return this.base + this.value + } +} + +@Injectable({ + providedIn: 'root' +}) +export class ExploreService { + spongEffectsService = inject(SpongEffectsService); + level$ = signal<'gene' | 'transcript'>('gene'); + diseaseNames$ = this.spongEffectsService.diseaseNames$; + selectedDisease$ = linkedSignal(() => this.diseaseNames$()[0]); +} diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.html b/src/app/routes/spongeffects/predict/form/predict-form.component.html new file mode 100644 index 00000000..4735c45a --- /dev/null +++ b/src/app/routes/spongeffects/predict/form/predict-form.component.html @@ -0,0 +1 @@ +

form works!

diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.scss b/src/app/routes/spongeffects/predict/form/predict-form.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts b/src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts new file mode 100644 index 00000000..2898f9d8 --- /dev/null +++ b/src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {PredictFormComponent} from './predict-form.component'; + +describe('FormComponent', () => { + let component: PredictFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PredictFormComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PredictFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.ts b/src/app/routes/spongeffects/predict/form/predict-form.component.ts new file mode 100644 index 00000000..851d8f1e --- /dev/null +++ b/src/app/routes/spongeffects/predict/form/predict-form.component.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-predict-form', + imports: [], + templateUrl: './predict-form.component.html', + styleUrl: './predict-form.component.scss' +}) +export class PredictFormComponent { + +} diff --git a/src/app/routes/spongeffects/predict/predict.component.html b/src/app/routes/spongeffects/predict/predict.component.html new file mode 100644 index 00000000..3d613d32 --- /dev/null +++ b/src/app/routes/spongeffects/predict/predict.component.html @@ -0,0 +1,140 @@ + + + +
1
+ Expression file upload + + Upload gene/transcript expression for cancer type classification + +
+ +
+
+

+ + +
+ + + + + + + +
{{ displayedColsValueMap.get(column) }}{{ emp[column] }}
+
+
+ + + + + Or drop your expression file here! + + + + {{ f.name }} + + +
+
+ +

+ + + + +
2
+ + Options + + + spongEffects prediction parameters + +
+ + +

+ +
Filtering thresholds:
+

+
+ + + multiple miRNA sensitivity correlation (mscor) + + Please provide a number between 0 and 10 + + + + False discovery rate (FDR) + + Please provide a number between 0 and 0.5 + + + + Minimal expression value + Please provide a number between 0 and 1.000 + + +

+ +
Further settings:
+

+ + Enrichment method + + {{method}} + + + + + Minimum module size + + Please provide a number between 0 and 5.000 + + + + Maximum module size + + Please provide a number between 0 and 5.000 + + + + Apply log2 scaling factor to uploaded expression + + + + Predict on subtype level (can lead to longer run times) + + +
+
+
+ +
+ + + +
\ No newline at end of file diff --git a/src/app/routes/spongeffects/predict/predict.component.scss b/src/app/routes/spongeffects/predict/predict.component.scss new file mode 100644 index 00000000..8ccf10d9 --- /dev/null +++ b/src/app/routes/spongeffects/predict/predict.component.scss @@ -0,0 +1,20 @@ +.circle { + width: 40px; + height: 40px; + line-height: 40px; + border-radius: 50%; + font-size: 20px; + color: white; + text-align: center; + background: #4071d1; + } + + .prediction-params { + width: 30%; + margin-right: 30px; + } + + .float-label-always ::ng-deep mat-form-field { + float: always; + } + \ No newline at end of file diff --git a/src/app/routes/spongeffects/predict/predict.component.spec.ts b/src/app/routes/spongeffects/predict/predict.component.spec.ts new file mode 100644 index 00000000..3d5efa22 --- /dev/null +++ b/src/app/routes/spongeffects/predict/predict.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PredictComponent } from './predict.component'; + +describe('PredictComponent', () => { + let component: PredictComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PredictComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PredictComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/predict/predict.component.ts b/src/app/routes/spongeffects/predict/predict.component.ts new file mode 100644 index 00000000..77c295fd --- /dev/null +++ b/src/app/routes/spongeffects/predict/predict.component.ts @@ -0,0 +1,388 @@ +import {Component, ElementRef, inject, ViewChild} from '@angular/core'; +import {MatCardModule} from '@angular/material/card'; +import {MatIconModule} from '@angular/material/icon'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatListModule} from '@angular/material/list'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatProgressBarModule, ProgressBarMode} from '@angular/material/progress-bar'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatChipsModule} from '@angular/material/chips'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatMenuModule} from '@angular/material/menu'; +import {NgxDropzoneModule} from 'ngx-dropzone'; +import {MatButtonModule} from '@angular/material/button'; +import {MatTableDataSource, MatTableModule} from '@angular/material/table'; +import {ExampleExpression, PlotlyData} from '../../../interfaces'; +import {CommonModule} from '@angular/common'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; +import {MatOption} from '@angular/material/core'; +import {timer} from 'rxjs'; +import {BackendService} from '../../../services/backend.service'; +import {VersionsService} from '../../../services/versions.service'; + +declare var Plotly: any; + + +const EXAMPLE_GENE_EXPR: ExampleExpression[] = [ + {id: "ENSG00000000233", sample1: 6, sample2: 5, sample3: 8, sample4: 2, sampleN: 1}, + {id: "ENSG00000000412", sample1: 2, sample2: 1, sample3: 2, sample4: 3, sampleN: 4}, + {id: "ENSG00000000442", sample1: 10, sample2: 9, sample3: 8, sample4: 0, sampleN: 7} +] + +@Component({ + selector: 'app-predict', + imports: [ + MatCardModule, + MatIconModule, + MatTabsModule, + MatSelectModule, + MatFormFieldModule, + MatInputModule, + MatSliderModule, + MatSlideToggleModule, + MatExpansionModule, + MatListModule, + MatDividerModule, + MatProgressBarModule, + MatProgressSpinnerModule, + MatTooltipModule, + MatChipsModule, + MatCheckboxModule, + MatRadioModule, + MatButtonToggleModule, + MatMenuModule, + NgxDropzoneModule, + MatButtonModule, + CommonModule, + MatTableModule, + FormsModule, + ReactiveFormsModule, + MatOption, + ], + templateUrl: './predict.component.html', + styleUrl: './predict.component.scss' +}) +export class PredictComponent { + backend = inject(BackendService); + versionService = inject(VersionsService); + + @ViewChild("classModelPerformancePlot") classPerformancePlotDiv!: ElementRef; + typePredictPiePlot!: ElementRef; + + + // example file + showExpressionExample = false; + exampleExpressionData: MatTableDataSource = new MatTableDataSource(EXAMPLE_GENE_EXPR); + displayedCols: string[] = ["id", "sample1", "sample2", "sample3", "sample4", "sampleN"]; + displayedColsValueMap: Map = new Map([ + ["id", ""], + ["sample1", "sample1"], ["sample2", "sample2"], ["sample3", "sample3"], ["sample4", "sample4"], ["sampleN", "sampleN"], + ]); + exampleExpressionFiles: File[] = [new File([JSON.stringify(EXAMPLE_GENE_EXPR)], "example_expression.txt")]; + // save example file + + + uploadedExpressionFiles: File[] = []; + + + // file params + filesToAccept: string = "text/*,application/*"; + maxFileSize: number = 100000000; + + + // prediction + predictionQueried: boolean = false; + predictionLoading: boolean = false; + timerRunning: boolean = false; + progressBarMode: ProgressBarMode = "determinate"; + progressBarValue: number = 0; + estimatedRunTime: number = 0; + + + // default parameters + mscorDefault: number = 0.1; + fdrDefault: number = 0.05; + minSizeDefault: number = 100; + maxSizeDefault: number = 2000; + minExprDefault: number = 10; + methods: string[] = ["gsva", "ssgsea", "OE"]; + methodDefault: string = this.methods[0]; + + logScaling: boolean = true; + predictSubtypes: boolean = false; + + // form controls + formGroup = new FormGroup({ + mscor: new FormControl(this.mscorDefault, [Validators.min(0), Validators.max(1)]), + fdr: new FormControl(this.fdrDefault, [Validators.min(0), Validators.max(1)]), + minSize: new FormControl(this.minSizeDefault, [Validators.min(0)]), + maxSize: new FormControl(this.maxSizeDefault, [Validators.min(0)]), + minExpr: new FormControl(this.minExprDefault, [Validators.min(0)]), + method: new FormControl(this.methodDefault), + logScaling: new FormControl(this.logScaling), + predictSubtypes: new FormControl(this.predictSubtypes), + }); + // paramsSignal = toSignal(this.formGroup.valueChanges); + + + // prediction data + predictionData: any; + predictionMeta: any; + predictedType: string = "None"; + predictedSubtype: string = "None"; + + + constructor() { + } + + + // setInitialParams = effect(() => { + // this.formGroup.setValue({ + // mscor: this.mscorDefault, + // fdr: this.fdrDefault, + // minSize: this.minSizeDefault, + // maxSize: this.maxSizeDefault, + // minExpr: this.minExprDefault, + // method: this.methodDefault, + // logScaling: this.logScaling, + // predictSubtypes: this.predictSubtypes + // }); + // }) + + + flipExampleExpression() { + this.showExpressionExample = !this.showExpressionExample; + } + + buttonText(btn: string) { + if (btn == "expr") { + return this.showExpressionExample ? "Hide example file" : "Show example file"; + } else { + return ""; + } + } + + expressionUploaded(): boolean { + return this.uploadedExpressionFiles.length > 0; + } + + onRemoveExpression(event: File) { + this.uploadedExpressionFiles.splice(this.uploadedExpressionFiles.indexOf(event), 1); + this.predictionQueried = false; + } + + onExpressionUpload(event: any) { + this.uploadedExpressionFiles.push(...event.addedFiles); + // TODO: check format + } + + acceptExpressionFiles(): string { + return this.expressionUploaded() ? "none" : this.filesToAccept; + } + + useExampleExpression(event: Event) { + event.stopPropagation(); + this.uploadedExpressionFiles.push(...this.exampleExpressionFiles); + } + + runButtonDisabled(): boolean { + return !this.expressionUploaded() || this.predictionLoading; + } + + estimateRunTime() { + const fileSize: number = this.uploadedExpressionFiles[0].size / (1024 ** 2); + const refSlope: number = 0.7; + const x0: number = 17; + const st: number = this.predictSubtypes ? 4 : 1; + return refSlope * fileSize + x0; + } + + async startTimer(): Promise { + this.timerRunning = true; + this.progressBarValue = 0; + const totalRunTime: number = this.estimateRunTime(); + this.estimatedRunTime = totalRunTime; + const interval: number = (1000 * totalRunTime) / 100; + const progressBarTimer = timer(0, interval); + progressBarTimer.subscribe(() => { + this.estimatedRunTime = totalRunTime * (100 - this.progressBarValue) / 100; + if (this.progressBarValue < 100) this.progressBarValue++; + }); + } + + getColorForValue(value: number): string { + let g: number = 140; + let r: number = value >= 0.5 ? Math.round(255 * 2 * (1 - value)) : 255; + const b: number = 0; + return `rgb(${r},${g},${b})`; + } + + + async extractPredictions(responseJson: any): Promise { + const typeGroups: Map = new Map(); + // group predictions by type + responseJson.data.forEach((entry: { typePrediction: string; subtypePrediction: string; }) => { + if (typeGroups.has(entry.typePrediction)) { + typeGroups.get(entry.typePrediction)?.push(entry.subtypePrediction); + } + }); + + const typeCounts: Map = new Map([...typeGroups.entries()].map(entry => { + return [entry[0], entry[1].length]; + })); + // sort by amount of samples + const sortedTypeCounts: Map = new Map([...typeCounts.entries()].sort((a, b) => a[1] - b[1])); + let x: number[] = [...sortedTypeCounts.values()]; + let y: string[] = [...sortedTypeCounts.keys()]; + // add model accuracy + let classPerformanceData = this.classPerformancePlotDiv.nativeElement.data; + // get modules data + classPerformanceData = classPerformanceData.filter((d: { name: string; }) => d.name == "modules") + if (classPerformanceData.length > 0) { + classPerformanceData = classPerformanceData[0]; + } + // create map to value + const classToMeasure: Map = new Map(); + for (let i = 0; i < classPerformanceData.x.length; i++) { + classToMeasure.set(classPerformanceData.x[i], classPerformanceData.y[i]); + } + const accValues: number[] = y.map(x_v => classToMeasure.get(x_v) ?? 0); // color based on balanced accuracy + const barColors: string[] = accValues.map(v => this.getColorForValue(v)); + // transform data + let data = [{ + x: x, + y: y, + text: accValues.map(v => "Balanced accuracy: " + v.toString()), + type: "bar", + name: "type", + orientation: "h", + marker: { + color: barColors + } + }]; + + // add subtype traces + if (this.predictSubtypes) { + const subtypeTraces: any[] = [...typeGroups.values()].map(sv => { + return { + x: sv.length, + y: y, + text: sv, + name: "subtypes", + orientation: "h" + } + }); + data.push(...subtypeTraces); + } + + const layout = { + paper_bgcolor: "white", + autosize: true, + barmode: "group", + margin: { + l: 250, + r: 25, + t: 50, + b: 50 + }, + xaxis: { + title: "Number of samples classified" + } + }; + const config = { + responsive: true + } + return {data: data, layout: layout, config: config}; + } + + async plotPredictions(plotlyData: PlotlyData): Promise { + Plotly.newPlot(this.typePredictPiePlot.nativeElement, plotlyData.data, plotlyData.layout, plotlyData.config); + } + + async processPredictions(predictionResponse: any): Promise { + // check response + if (!predictionResponse.ok) { + throw new Error(`File upload failed with status code: ${predictionResponse.status}`); + } + // save results + const predictionData = await predictionResponse.json(); + this.predictionData = predictionData.data; + this.predictionMeta = predictionData.meta[0]; + this.predictedType = predictionData.meta[0].type_predict; + this.predictedSubtype = predictionData.meta[0].subtype_predict; + // plot predictions + this.extractPredictions(predictionData) + .then(data => this.plotPredictions(data)); + } + + showError(message: string) { + // Implement your error display logic here, e.g., using a snackbar or modal + alert(message); // Simple alert for demonstration + } + + validateFileContent(file: File): boolean { + // Perform basic validation, e.g., check file type and size + // if (file.type !== this.acceptExpressionFiles()) { + // this.showError('Invalid file type. Please upload a text file.'); + // return false; + // } + if (file.size > this.maxFileSize) { + this.showError('File size exceeds the maximum limit.'); + return false; + } + // Additional content validation can be added here + return true; + } + + async getPredictionData(): Promise { + const uploadedFile: File = this.uploadedExpressionFiles[0]; + // Client-side validation + if (!this.validateFileContent(uploadedFile)) { + return Promise.reject('Client-side validation failed.'); + } + // send file and parameters to API and return response + try { + const prediction = await this.backend.predictCancerType( + this.versionService.versionReadOnly()(), + uploadedFile, this.predictSubtypes, this.logScaling, + this.formGroup.value.mscor ?? this.mscorDefault, + this.formGroup.value.fdr ?? this.fdrDefault, + this.formGroup.value.minSize ?? this.minSizeDefault, + this.formGroup.value.maxSize ?? this.maxSizeDefault, + this.formGroup.value.minExpr ?? this.minExprDefault, + this.formGroup.value.method ?? this.methodDefault, + ) + return prediction; + } catch (error) { + this.showError('Server-side validation failed. Please check the file content.'); + throw error; + } + } + + async predict() { + this.predictionQueried = true; + this.predictionLoading = true; + // start timer of estimated run time + this.startTimer().then(_ => this.timerRunning = false); + // start workflow + try { + const data = await this.getPredictionData(); + await this.processPredictions(data); + } catch (error) { + console.error(error); + } finally { + this.predictionLoading = false; + } + } +} + + diff --git a/src/app/routes/spongeffects/spongeffects.component.html b/src/app/routes/spongeffects/spongeffects.component.html new file mode 100644 index 00000000..5975c0cd --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.html @@ -0,0 +1,44 @@ + + + +

spongEffects can be used for downstream analysis of + SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random + forest + machine learning approach.

+

For more detailed information refer to the publication. +

+

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that + feature sufficient cancer subtyping for classification.

+

+ The tab below shows the spongEffects prediction results for a selected TCGA project. + You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their + gene/transcript expression. +

+

+ We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent + input data that can be uploaded via the + tab below. +

+

The model will predict a tumor type for every sample in the uploaded expression file individually.

+
+
+ + Explore + Predict + +
+ @if (mode() == 'explore') { + + } @else { + + } +
+ + @if (mode() == 'explore') { + + } @else { + + } + +
diff --git a/src/app/routes/spongeffects/spongeffects.component.scss b/src/app/routes/spongeffects/spongeffects.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/spongeffects.component.spec.ts b/src/app/routes/spongeffects/spongeffects.component.spec.ts new file mode 100644 index 00000000..740ea223 --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SpongEffectsComponent } from './spongeffects.component'; + +describe('SpongEffectsComponent', () => { + let component: SpongEffectsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SpongEffectsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SpongEffectsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/spongeffects.component.ts b/src/app/routes/spongeffects/spongeffects.component.ts new file mode 100644 index 00000000..6854ea94 --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.ts @@ -0,0 +1,44 @@ +import {Component, inject, model, resource} from '@angular/core'; +import {MatDrawer, MatDrawerContainer, MatDrawerContent} from "@angular/material/sidenav"; +import {MatButtonToggle, MatButtonToggleGroup} from "@angular/material/button-toggle"; +import {ExploreComponent} from "./explore/explore.component"; +import {PredictComponent} from "./predict/predict.component"; +import {VersionsService} from "../../services/versions.service"; +import {BackendService} from "../../services/backend.service"; +import {PredictFormComponent} from "./predict/form/predict-form.component"; +import {ExploreFormComponent} from "./explore/form/explore-form.component"; +import {InfoComponent} from "../../components/info/info.component"; + +// import { Tab, Cancer, PlotlyData } from '../../models/spongeffects.model'; + +@Component({ + selector: 'app-spongeffects', + templateUrl: './spongeffects.component.html', + imports: [ + MatDrawer, + MatDrawerContainer, + MatDrawerContent, + MatButtonToggleGroup, + MatButtonToggle, + ExploreComponent, + PredictComponent, + PredictFormComponent, + PredictFormComponent, + ExploreFormComponent, + InfoComponent + ], + styleUrls: ['./spongeffects.component.scss'] +}) +export class SpongEffectsComponent { + versionsService = inject(VersionsService); + backend = inject(BackendService); + version$ = this.versionsService.versionReadOnly(); + mode = model<'explore' | 'predict'>('explore'); + + spongeEffectsRuns = resource({ + request: this.version$, + loader: (version) => ( + this.backend.getSpongEffectsRuns(version.request) + ) + }) +} diff --git a/src/app/services/backend.service.ts b/src/app/services/backend.service.ts index 3c824d72..40bdde8e 100644 --- a/src/app/services/backend.service.ts +++ b/src/app/services/backend.service.ts @@ -3,7 +3,13 @@ import {HttpService} from "./http.service"; import { AlternativeSplicingEvent, BrowseQuery, + CeRNA, + CeRNAExpression, + CeRNAInteraction, + CeRNAQuery, Dataset, + DatasetInfo, + EnrichmentScoreDistributions, Gene, GeneCount, GeneExpression, @@ -14,7 +20,16 @@ import { GOTerm, Hallmark, OverallCounts, + PlotData, + PredictCancerType, + RunClassPerformance, RunInfo, + RunPerformance, + SpongEffectsGeneModuleMembers, + SpongEffectsGeneModules, + SpongEffectsRun, + SpongEffectsTranscriptModuleMembers, + SpongEffectsTranscriptModules, SurvivalPValue, SurvivalRate, TranscriptExpression, @@ -177,18 +192,6 @@ export class BackendService { return this.http.getRequest(this.getRequestURL(route, query)); } - async getSurvivalPValues(version: number, ensgs: string[], disease: Dataset): Promise { - const route = 'survivalAnalysis/getPValues'; - - const query: Query = { - sponge_db_version: version, - disease_name: disease.disease_name, - dataset_ID: disease.dataset_ID, - ensg_number: ensgs.join(',') - } - - return (await this.http.getRequest(this.getRequestURL(route, query))) ?? []; - } getAutocomplete(version: number, query: string): Promise { if (query.length < 2) { @@ -334,4 +337,140 @@ export class BackendService { private getRequestURL(route: string, query: Query): string { return `${BackendService.API_BASE}/${route}?${this.stringify(query)}`; } + + + // getCeRNA(query: CeRNAQuery): Promise { + // const sponge_db_version = this.versionService.getCurrentVersion(); + // let request = BackendService.API_BASE + '/findceRNA?disease_name=' + query.disease.disease_name + `?sponge_db_version=${sponge_db_version}`; + + // request += `&minBetweenness=${query.minBetweenness}`; + // request += `&minNodeDegree=${query.minDegree}`; + // request += `&minEigenvector=${query.minEigen}`; + // request += `&sorting=${query.geneSorting}`; + // request += `&descending=${true}`; + // request += `&limit=${query.maxGenes}`; + + // return this.http.getRequest(request); + // } + + getCeRNAInteractionsAll(disease: string, maxPValue: number, ensgs: string[], limit?: number, offset?: number): Promise { + let request = BackendService.API_BASE + '/ceRNAInteraction/findAll?disease_name=' + disease; + request += `&ensg_number=${ensgs.join(',')}`; + request += `&pValue=${maxPValue}`; + + if (limit) { + request += `&limit=${limit}`; + } + if (offset) { + request += `&offset=${offset}`; + } + + return this.http.getRequest(request); + } + + getCeRNAInteractionsSpecific(disease: string, maxPValue: number, ensgs: string[]): Promise { + let request = BackendService.API_BASE + '/ceRNAInteraction/findSpecific?disease_name=' + disease; + request += `&ensg_number=${ensgs.join(',')}`; + request += `&pValue=${maxPValue}`; + + return this.http.getRequest(request); + } + + getCeRNAExpression(ensgs: string[], diseaseName: string): Promise { + let request = BackendService.API_BASE + '/exprValue/getceRNA?disease_name=' + diseaseName; + request += `&ensg_number=${ensgs.join(',')}`; + + return this.http.getRequest(request); + } + + getTranscriptExpression(ensts: string[], disease_name?: string): Promise { + let request = BackendService.API_BASE + `/exprValue/getTranscript?disease_name=${disease_name}`; + request += `&enst_number=${ensts.join(',')}`; + + return this.http.getRequest(request); + } + + async getSurvivalPValues(version: number, ensgs: string[], disease: Dataset): Promise { + const route = 'survivalAnalysis/getPValues'; + + const query: Query = { + sponge_db_version: version, + disease_name: disease.disease_name, + dataset_ID: disease.dataset_ID, + ensg_number: ensgs.join(',') + } + + return (await this.http.getRequest(this.getRequestURL(route, query))) ?? []; + } + + +// spongEffects services: + + getSpongEffectsRuns(version: number, dataset_ID?: number, diseaseName?: string): Promise { + const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsRuns?` + + (dataset_ID ? `?dataset_ID=${dataset_ID}` : '') + + (diseaseName ? `&disease_name=${diseaseName}` : '') + + `&sponge_db_version=${version}` + return this.http.getRequest(request); + } + + getRunPerformance(version: number, diseaseName: string, level: string): Promise { + const request = BackendService.API_BASE + '/spongEffects/getRunPerformance' + `?disease_name=${diseaseName}` + `&level=${level}` + `&sponge_db_version=${version}`; + return this.http.getRequest(request); + } + + getRunClassPerformance(version: number, diseaseName: string, level: string): Promise { + const request = BackendService.API_BASE + '/spongEffects/getRunClassPerformance' + `?disease_name=${diseaseName}` + `&level=${level}` + `&sponge_db_version=${version}`; + return this.http.getRequest(request); + } + + getEnrichmentScoreDistributions(version: number,diseaseName: string, level: string): Promise { + const request = `${BackendService.API_BASE}/spongEffects/getEnrichmentScoreDistributions?disease_name=${diseaseName}&level=${level}&sponge_db_version=${version}`; + return this.http.getRequest(request); + } + + getSpongEffectsGeneModules(version: number, diseaseName: string): Promise { + const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsGeneModules?disease_name=${diseaseName}&sponge_db_version=${version}`; + return this.http.getRequest(request); + } + + getSpongEffectsGeneModuleMembers(version: number, diseaseName: string, ensgNumber?: string, geneSymbol?: string): Promise { + let request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsGeneModuleMembers?disease_name=${diseaseName}&sponge_db_version=${version}`; + if (ensgNumber) { + request += `&ensg_number=${ensgNumber}`; + } + if (geneSymbol) { + request += `&gene_symbol=${geneSymbol}`; + } + return this.http.getRequest(request); + } + + getSpongEffectsTranscriptModules(version: number, diseaseName: string): Promise { + const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsTranscriptModules?disease_name=${diseaseName}&sponge_db_version=${version}`; + return this.http.getRequest(request); + } + + getSpongEffectsTranscriptModuleMembers(version: number, diseaseName: string, enstNumber?: string): Promise { + let request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsTranscriptModuleMembers?disease_name=${diseaseName}&sponge_db_version=${version}`; + if (enstNumber) { + request += `&enst_number=${enstNumber}`; + } + return this.http.getRequest(request); + } + + predictCancerType(version: number, file: Blob, subtypes: boolean, log: boolean, mscor: number, fdr: number, minSize: number, maxSize: number, minExpr: number, method: string): Promise { + const formData = new FormData(); + formData.append('file', file); + formData.append('subtypes', subtypes.toString()); + formData.append('log', log.toString()); + formData.append('mscor', mscor.toString()); + formData.append('fdr', fdr.toString()); + formData.append('min_size', minSize.toString()); + formData.append('max_size', maxSize.toString()); + formData.append('min_expr', minExpr.toString()); + formData.append('method', method); + const request = `${BackendService.API_BASE}/spongEffects/predictCancerType?sponge_db_version=${version}`; + return this.http.postRequest(request, formData); + } + } diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts index 32b86bf2..db0ed621 100644 --- a/src/app/services/http.service.ts +++ b/src/app/services/http.service.ts @@ -24,7 +24,7 @@ export class HttpService { } async postRequest(request: string, payload: {}): Promise { - const headers = new HttpHeaders({'Content-Type': 'application/json'}); + const headers = payload instanceof FormData ? {} : new HttpHeaders({ 'Content-Type': 'application/json' }); try { return lastValueFrom(this.http.post(request, payload, {headers: headers})); } catch (error) { diff --git a/src/app/services/spong-effects.service.spec.ts b/src/app/services/spong-effects.service.spec.ts new file mode 100644 index 00000000..0fb1f25e --- /dev/null +++ b/src/app/services/spong-effects.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SpongEffectsService } from './spong-effects.service'; + +describe('SpongEffectsService', () => { + let service: SpongEffectsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SpongEffectsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/spong-effects.service.ts b/src/app/services/spong-effects.service.ts new file mode 100644 index 00000000..7663eb54 --- /dev/null +++ b/src/app/services/spong-effects.service.ts @@ -0,0 +1,24 @@ +import {computed, inject, Injectable, resource} from '@angular/core'; +import {BackendService} from "./backend.service"; +import {VersionsService} from "./versions.service"; +import {SpongEffectsRun} from "../interfaces"; + +@Injectable({ + providedIn: 'root' +}) +export class SpongEffectsService { + backend = inject(BackendService); + versionsService = inject(VersionsService); + diseaseNames$ = computed(() => { + const runs = this.spongEffectsRuns$.value() || []; + return runs.map((run: SpongEffectsRun) => run.disease_name) + .filter((value: string, index: number, self: Array) => self.indexOf(value) === index); + }); + private readonly _version$ = this.versionsService.versionReadOnly(); + spongEffectsRuns$ = resource({ + request: this._version$, + loader: async (version) => ( + await this.backend.getSpongEffectsRuns(version.request) + ) + }); +}