diff --git a/src/app/analysis.service.ts b/src/app/analysis.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..394d81e8b7cd4e2ee9da5b8dfed7407b9ca9c8e2 --- /dev/null +++ b/src/app/analysis.service.ts @@ -0,0 +1,47 @@ +import {Injectable} from '@angular/core'; +import {ProteinGroup} from './pages/protein-network'; +import {Subject} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AnalysisService { + + private selectedProteins = new Map<string, ProteinGroup>(); + private selectSubject = new Subject<{protein: ProteinGroup, selected: boolean}>(); + + constructor() { + } + + addProtein(protein: ProteinGroup) { + if (!this.inSelection(protein)) { + this.selectedProteins.set(`${protein.groupId}`, protein); + this.selectSubject.next({protein, selected: true}); + } + } + + inSelection(protein: ProteinGroup): boolean { + return this.selectedProteins.has(`${protein.groupId}`); + } + + removeProtein(protein: ProteinGroup) { + if (this.selectedProteins.delete(`${protein.groupId}`)) { + this.selectSubject.next({protein, selected: false}); + } + } + + getSelection(): ProteinGroup[] { + return Array.from(this.selectedProteins.values()); + } + + getCount(): number { + return this.selectedProteins.size; + } + + subscribe(cb: (protein: ProteinGroup, selected: boolean) => void) { + this.selectSubject.subscribe((event) => { + cb(event.protein, event.selected); + }); + } + +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ed0291588b7b6a66abfd8b71d7a35c9a4f78125f..f00fc9cc0e0c62bf7010b0f8d8a624e6224cf74f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,6 +10,7 @@ import {ExplorerPageComponent} from './pages/explorer-page/explorer-page.compone import {AboutPageComponent} from './pages/about-page/about-page.component'; import {HomePageComponent} from './pages/home-page/home-page.component'; import {HttpClientModule} from '@angular/common/http'; +import {ProteinAnalysisComponent} from './components/protein-analysis/protein-analysis.component'; @NgModule({ declarations: [ @@ -17,6 +18,7 @@ import {HttpClientModule} from '@angular/common/http'; ExplorerPageComponent, AboutPageComponent, HomePageComponent, + ProteinAnalysisComponent, ], imports: [ BrowserModule, diff --git a/src/app/components/protein-analysis/protein-analysis.component.html b/src/app/components/protein-analysis/protein-analysis.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9ebbb67366c753071d66c5f023582477d3f629ba --- /dev/null +++ b/src/app/components/protein-analysis/protein-analysis.component.html @@ -0,0 +1,40 @@ +<div class="modal" [class.is-active]="show"> + <div class="modal-background"></div> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">Launch Protein Analysis</p> + <button class="delete" aria-label="close" (click)="close()"></button> + </header> + <section class="modal-card-body"> + <h4 class="title is-4">Selection</h4> + <table class="table"> + <thead> + <tr> + <td>AC</td> + <td>Actions</td> + </tr> + </thead> + <tbody> + <tr *ngFor="let p of analysis.getSelection()"> + <td>{{p.name}}</td> + <td> + <button (click)="analysis.removeProtein(p)" class="button is-small is-danger"> + <i class="fa fa-trash"></i> + </button> + </td> + </tr> + </tbody> + </table> + </section> + <footer class="modal-card-foot"> + <button class="button is-success"> + <span class="icon"><i class="fa fa-play"></i></span> + <span>Multi Steiner</span> + </button> + <button class="button is-success"> + <span class="icon"><i class="fa fa-play"></i></span> + <span>Key Pathway Miner</span> + </button> + </footer> + </div> +</div> diff --git a/src/app/components/protein-analysis/protein-analysis.component.scss b/src/app/components/protein-analysis/protein-analysis.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/components/protein-analysis/protein-analysis.component.ts b/src/app/components/protein-analysis/protein-analysis.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..165b934e2fdffb3756ac67022d3074244e317dc8 --- /dev/null +++ b/src/app/components/protein-analysis/protein-analysis.component.ts @@ -0,0 +1,28 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {AnalysisService} from '../../analysis.service'; + +@Component({ + selector: 'app-protein-analysis', + templateUrl: './protein-analysis.component.html', + styleUrls: ['./protein-analysis.component.scss'] +}) +export class ProteinAnalysisComponent implements OnInit { + + @Input() + public show = false; + + @Output() + public showChange = new EventEmitter<boolean>(); + + constructor(public analysis: AnalysisService) { + } + + ngOnInit(): void { + } + + public close() { + this.show = false; + this.showChange.emit(this.show); + } + +} diff --git a/src/app/pages/explorer-page/explorer-page.component.html b/src/app/pages/explorer-page/explorer-page.component.html index cd02afdbcdf97436b16ab5d5f16e1d6510fc1fc5..7038f2ebbf5dc0903d5fe95d306e2d8b9196ccb2 100644 --- a/src/app/pages/explorer-page/explorer-page.component.html +++ b/src/app/pages/explorer-page/explorer-page.component.html @@ -1,3 +1,5 @@ +<app-protein-analysis [(show)]="showAnalysisDialog"></app-protein-analysis> + <div class="content explorer"> <div class="content bar-left"> @@ -103,6 +105,26 @@ <div class="content bar-right"> + <div class="card bar"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="fas fa-flask" aria-hidden="true"></i> + </span> Analysis + </p> + </header> + <div class="card-content"> + <button (click)="showAnalysisDialog = true" class="button is-success" [disabled]="analysis.getCount() === 0"> + <span class="icon"> + <i class="fa fa-list"></i> + </span> + <span> + Open Protein Selection + </span> + </button> + </div> + </div> + <div class="card bar"> <header class="card-header"> <p class="card-header-title"> @@ -145,6 +167,8 @@ <figure class="image"> <img src="assets/boxplot.png" alt="Boxplots"> </figure> + <button class="button" *ngIf="!inSelection(proteinGroup)" (click)="addToSelection(proteinGroup)">Select for analysis</button> + <button class="button" *ngIf="inSelection(proteinGroup)" (click)="removeFromSelection(proteinGroup)">Remove from analysis</button> </div> </div> </div> diff --git a/src/app/pages/explorer-page/explorer-page.component.ts b/src/app/pages/explorer-page/explorer-page.component.ts index 30ffde5a1e235ae1a4b74bc414b870d760674a26..752d653616a68260afb2d612f9868d62da82a7ee 100644 --- a/src/app/pages/explorer-page/explorer-page.component.ts +++ b/src/app/pages/explorer-page/explorer-page.component.ts @@ -1,8 +1,9 @@ import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; -import {Effect, ProteinNetwork} from '../protein-network'; +import {Effect, ProteinGroup, ProteinNetwork} from '../protein-network'; import {HttpClient} from '@angular/common/http'; import {ApiService} from '../../api.service'; +import {AnalysisService} from '../../analysis.service'; declare var vis: any; @@ -19,7 +20,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { public proteinGroup = ''; public proteinNames: Array<string> = []; public proteinACs: Array<string> = []; - public baitNames: Array<string> = []; public baitProteins: Array<{ checked: boolean; data: Effect }> = []; @@ -39,18 +39,18 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { private dumpPositions = false; public physicsEnabled = false; + public showAnalysisDialog = false; + @ViewChild('network', {static: false}) networkEl: ElementRef; - constructor(private http: HttpClient, private route: ActivatedRoute, private router: Router, private api: ApiService) { - this.groupId = 'IFI16'; + constructor(private http: HttpClient, + private route: ActivatedRoute, + private router: Router, + private api: ApiService, + public analysis: AnalysisService) { this.geneNames.push('IFI16'); this.proteinNames.push('Gamma-interface-inducible protein 16'); this.proteinACs.push('Q16666'); - this.baitNames.push('Bait Protein 1'); - this.baitNames.push('Bait Protein 2'); - this.baitNames.push('Bait Protein 3'); - this.baitNames.push('Bait Protein 4'); - this.baitNames.push('Bait Protein 5'); this.route.queryParams.subscribe(async (params) => { this.dumpPositions = params.dumpPositions; @@ -79,6 +79,23 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { // this.zoomToNode(proteinGroup) this.showDetails = true; }); + + this.analysis.subscribe((protein, selected) => { + const nodeId = `pg_${protein.groupId}`; + if (selected) { + const node = this.nodeData.nodes.get(nodeId); + if (node) { + node.color = '#c42eff'; + this.nodeData.nodes.update(node); + } + } else { + const node = this.nodeData.nodes.get(nodeId); + if (node) { + node.color = '#e2b600'; + this.nodeData.nodes.update(node); + } + } + }); } ngOnInit() { @@ -104,7 +121,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { this.filterNodes(); } - public zoomToNode(id: string) { + private zoomToNode(id: string) { const coords = this.network.getPositions(id)[id]; this.network.moveTo({ position: {x: coords.x, y: coords.y}, @@ -117,9 +134,11 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { return this.groupId; } - public async openSummary(groupId: string) { + public async openSummary(groupId: string, zoom: boolean) { await this.router.navigate(['explorer'], {queryParams: {proteinGroup: groupId}}); - this.zoomToNode(this.proteinGroup); + if (zoom) { + this.zoomToNode(this.proteinGroup); + } } public async closeSummary() { @@ -170,7 +189,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { console.log(id); if (id.length > 0) { console.log('clicked node:', id); - this.openSummary(id[0]); + this.openSummary(id[0], false); } }); @@ -239,17 +258,21 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { }); } - private mapProteinGroupToNode(proteinGroup: any): any { + private mapProteinGroupToNode(proteinGroup: ProteinGroup): any { + let color = '#e2b600'; + if (this.analysis.inSelection(proteinGroup)) { + color = '#c42eff'; + } return { id: `pg_${proteinGroup.groupId}`, label: `${proteinGroup.name}`, - size: 10, font: '5px', color: '#e2b600', shape: 'ellipse', shadow: false, + size: 10, font: '5px', color, shape: 'ellipse', shadow: false, x: proteinGroup.x, y: proteinGroup.y }; } - private mapEffectToNode(effect: any): any { + private mapEffectToNode(effect: Effect): any { return { id: `eff_${effect.name}`, label: `${effect.name}`, @@ -260,7 +283,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { } private mapEdge(edge: any): any { - return {from: `pg_${edge.groupId}`, to: `eff_${edge.effectName}`, color: { color: '#afafaf', highlight: '#854141' }}; + return {from: `pg_${edge.groupId}`, to: `eff_${edge.effectName}`, color: {color: '#afafaf', highlight: '#854141'}}; } private mapDataToNodes(data: ProteinNetwork): { nodes: any[], edges: any[] } { @@ -291,4 +314,34 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { return x - Math.floor(x); } + // Selection + // TODO: Improve usage of group ids, revise this after models have been changed to just protein + + inSelection(groupIdStr: string): boolean { + if (!this.proteinData || !groupIdStr) { + return false; + } + const groupId = Number(groupIdStr.split('_')[1]); + const protein = this.proteinData.getProteinGroup(groupId); + return this.analysis.inSelection(protein); + } + + addToSelection(groupIdStr: string) { + if (!groupIdStr) { + return; + } + const groupId = Number(groupIdStr.split('_')[1]); + const protein = this.proteinData.getProteinGroup(groupId); + this.analysis.addProtein(protein); + } + + removeFromSelection(groupIdStr: string) { + if (!groupIdStr) { + return; + } + const groupId = Number(groupIdStr.split('_')[1]); + const protein = this.proteinData.getProteinGroup(groupId); + this.analysis.removeProtein(protein); + } + }