From 020746890b1e3bf48125cc80c8fea301ad597a88 Mon Sep 17 00:00:00 2001
From: Julian Matschinske <ge93nar@mytum.de>
Date: Wed, 1 Apr 2020 13:47:27 +0200
Subject: [PATCH] Protein selection

---
 src/app/analysis.service.ts                   | 47 ++++++++++
 src/app/app.module.ts                         |  2 +
 .../protein-analysis.component.html           | 40 +++++++++
 .../protein-analysis.component.scss           |  0
 .../protein-analysis.component.ts             | 28 ++++++
 .../explorer-page.component.html              | 24 +++++
 .../explorer-page/explorer-page.component.ts  | 87 +++++++++++++++----
 7 files changed, 211 insertions(+), 17 deletions(-)
 create mode 100644 src/app/analysis.service.ts
 create mode 100644 src/app/components/protein-analysis/protein-analysis.component.html
 create mode 100644 src/app/components/protein-analysis/protein-analysis.component.scss
 create mode 100644 src/app/components/protein-analysis/protein-analysis.component.ts

diff --git a/src/app/analysis.service.ts b/src/app/analysis.service.ts
new file mode 100644
index 00000000..394d81e8
--- /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 ed029158..f00fc9cc 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 00000000..9ebbb673
--- /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 00000000..e69de29b
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 00000000..165b934e
--- /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 cd02afdb..7038f2eb 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 30ffde5a..752d6536 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);
+  }
+
 }
-- 
GitLab