From 073b358834d049ae1d05ccd5b3a1eafb8e3dda4e Mon Sep 17 00:00:00 2001
From: Michael Hartung <michi@Michaels-MacBook-Pro.local>
Date: Fri, 9 Jul 2021 20:43:14 +0200
Subject: [PATCH] node styles, handle seed node styles and different user
 inputs properly, complement user inouts where necessary; using deep copy
 where necessary to fix missing style updates; load expression information and
 use opacity to display it to also affect images; toggle drug view in explorer
 network

---
 src/app/config.ts                             | 64 +++++++++---
 src/app/interfaces.ts                         | 21 ++--
 src/app/main-network.ts                       | 25 +++--
 src/app/network-settings.ts                   | 20 ++--
 .../explorer-page.component.html              |  5 +
 .../explorer-page/explorer-page.component.ts  | 99 +++++++++++++------
 .../netex-controller.service.ts               | 14 +++
 7 files changed, 178 insertions(+), 70 deletions(-)

diff --git a/src/app/config.ts b/src/app/config.ts
index 7c29acf5..7b6e0978 100644
--- a/src/app/config.ts
+++ b/src/app/config.ts
@@ -1,7 +1,7 @@
 // https://visjs.github.io/vis-network/docs/network/nodes.html
 export interface NodeGroup {
-  groupName: string;
-  color?: string;
+  groupName?: string;
+  color?: any;
   shape?: 'circle' | 'triangle' | 'star' | 'square' | 'image' | 'text' | 'ellipse' | 'box' | 'diamond' | 'dot';
   type?: string;
   image?: string;
@@ -11,6 +11,7 @@ export interface NodeGroup {
   highlight?: any;
   borderWidth?: number;
   borderWidthSelected?: number;
+  background?: any;
 }
 
 export interface EdgeGroup {
@@ -84,16 +85,24 @@ export const defaultConfig: IConfig = {
   interactionProteinProtein: 'STRING',
   nodeGroups: {
     // all NodeGroups but the default group must be set, if not provided by the user, they will be taken from here
+    // IMPORTANT: node color must be hexacode!
     default: {
       // this default group is used for default node group values
       // and is fallback in case user does not provide any nodeGroup
       groupName: 'Default Node Group',
-      color: '#FFFF00',
+      color: {
+        border: '#FFFF00',
+        background: '#FFFF00',
+        highlight: {
+          border: '#FF0000',
+          background: '#FF0000'
+        },
+      },
       shape: 'triangle',
       type: 'default type',
       detailShowLabel: false,
       font: {
-        color: 'black',
+        color: '#000000',
         size: 14,
         face: 'arial',
         background: undefined,
@@ -106,29 +115,46 @@ export const defaultConfig: IConfig = {
         mono: false,
       },
       borderWidth: 1,
-      borderWidthSelected: 3
+      borderWidthSelected: 2
     },
     foundNode: {
       groupName: 'Found Nodes',
-      color: 'red',
+      color: {
+        border: '#F12590',
+        background: '##F12590',
+        highlight: {
+          border: '#F12590',
+          background: '#F12590'
+        },
+      },
       shape: 'circle',
       type: 'default node type',
     },
     foundDrug: {
       groupName: 'Found Drugs',
-      color: 'green',
+      color: {
+        border: '#F12590',
+        background: '##F12590',
+        highlight: {
+          border: '#F12590',
+          background: '#F12590'
+        },
+      },
       shape: 'star',
       type: 'default drug type',
     },
     seedNode: {
-      groupName: 'Seed Nodes',
+      // groupName: 'Seed Nodes',
       // color: '#F8981D',
       // shape: 'circle',
       // type: 'seed',
-      border: '#F8981D',
-      highlight: {
+      color: {
         border: '#F8981D',
-        background: '#F8981D'
+        background: '#F8981D',
+        highlight: {
+          border: '#F8981D',
+          background: '#F8981D'
+        },
       },
       font: {
         color: '#F8981D',
@@ -136,14 +162,20 @@ export const defaultConfig: IConfig = {
       }
     },
     selectedNode: {
-      groupName: 'Selected Nodes',
-      color: '#F8981D',
+      // groupName: 'Selected Nodes',
+      // color: '#F8981D',
       // shape: 'dot',
       // type: 'selected',
-      border: '#F8981D',
-      highlight: {
+
+      borderWidth: 3,
+      borderWidthSelected: 4,
+      color: {
         border: '#F8981D',
-        background: '#F8981D'
+        // background: '#F8981D',
+        highlight: {
+          border: '#F8981D',
+        //   background: '#F8981D'
+        },
       },
       font: {
         color: '#F8981D',
diff --git a/src/app/interfaces.ts b/src/app/interfaces.ts
index 35d6ca2d..4871bace 100644
--- a/src/app/interfaces.ts
+++ b/src/app/interfaces.ts
@@ -18,6 +18,7 @@ export interface Node {
   y?: number;
   borderWidth: number;
   borderWidthSelected: number;
+  opacity?: number;
   font: {
     color: string;
     size: number;
@@ -117,11 +118,19 @@ export function getNodeId(node: Node) {
   /**
    * Returns backend_id of Gene object
    */
-   if ('netexId' in node) {
-     return node['netexId']
-   } else {
-     return node.id
-   }
+  //  if ('netexId' in node) {
+  //    return node['netexId']
+  //  } else {
+  //    return node.id
+  //  }
+  return node.id
+}
+
+export function getNetworkId(node: Node) {
+  /**
+   * Returns ID of a network node
+   */
+  return node.id
 }
 
 export function getId(gene: Node) {
@@ -150,7 +159,7 @@ export function getWrapperFromNode(gene: Node): Wrapper {
   // if node does not have property group, it was found by the analysis
   gene.group = gene.group ? gene.group : 'foundNode';
   return {
-    id: getNodeId(gene),
+    id: getNetworkId(gene),
     data: gene,
   };
 }
diff --git a/src/app/main-network.ts b/src/app/main-network.ts
index 6e329530..147d1bff 100644
--- a/src/app/main-network.ts
+++ b/src/app/main-network.ts
@@ -1,5 +1,6 @@
 import { defaultConfig, IConfig } from './config';
 import {NodeInteraction, Node, getProteinNodeId} from './interfaces';
+import * as merge from 'lodash/fp/merge'; 
 
 export function getDatasetFilename(dataset: Array<[string, string]>): string {
   return `network-${JSON.stringify(dataset).replace(/[\[\]\",]/g, '')}.json`;
@@ -50,7 +51,7 @@ export class ProteinNetwork {
    * @param config 
    * @returns 
    */
-  private mapCustomNode(customNode: any, config: IConfig): Node {
+  public mapCustomNode(customNode: any, config: IConfig): Node {
     let node;
     if (customNode.group === undefined) {
       // fallback to default node
@@ -64,17 +65,23 @@ export class ProteinNetwork {
       node = JSON.parse(JSON.stringify(config.nodeGroups[customNode.group]));
     }
     // update the node with custom node properties, including values fetched from backend
-    node = {
-      ...node,
-      ...customNode
-    }
+    node = merge(node, customNode)
     // label is only used for network visualization
     node.label = customNode.label ? customNode.label : customNode.id;
     if (node.image) {
       node.shape = 'image';
     }
-    // // remove '_' from group if group is defined
-    // node.group = node.group===undefined ? node.group : node.group.replace('_', '');
+    // if color is set as string, add detail settings
+    if (typeof node.color === 'string') {
+      node.color = {
+        border: node.color,
+        background: node.color,
+        highlight: {
+          border: node.color,
+          background: node.color
+        }
+      }
+    }
     return node;
   }
 
@@ -86,7 +93,7 @@ export class ProteinNetwork {
    * @param config 
    * @returns 
    */
-  private mapCustomEdge(customEdge: NodeInteraction, config: IConfig): any {
+  public mapCustomEdge(customEdge: NodeInteraction, config: IConfig): any {
     let edge;
     if (customEdge.group === undefined) {
       // fallback to default node
@@ -102,8 +109,6 @@ export class ProteinNetwork {
       ...edge,
       ...customEdge
     }
-    // // remove '_' from group if group is defined
-    // edge.group = edge.group===undefined ? edge.group : edge.group.replace('_', '');
     return edge;
   }
 
diff --git a/src/app/network-settings.ts b/src/app/network-settings.ts
index 29b75b74..acdf8bb9 100644
--- a/src/app/network-settings.ts
+++ b/src/app/network-settings.ts
@@ -125,10 +125,12 @@ export class NetworkSettings {
     config: IConfig,
     isSeed: boolean,
     isSelected: boolean,
-    gradient?: number): any {
+    gradient: number = 1): any {
 
+      // delete possible old styles
+      Object.keys(defaultConfig.nodeGroups.default).forEach(e => delete node[e]);
+      // set group styles
       if (node.group === 'default') {
-        console.log("we should not see this")
         node = merge(node, defaultConfig.nodeGroups.default);
       } else {
         node = merge(node, config.nodeGroups[node.group]);
@@ -136,26 +138,24 @@ export class NetworkSettings {
       // note that seed and selected node style are applied after the node style is fetched. 
       // this allows to overwrite only attributes of interest, therefor in e.g. seedNode group
       // certain attributes like shape can remain undefined
+      // use lodash merge to not lose deep attributes, e.g. "font.size"
       if (isSeed) {
         // apply seed node style to node
         node = merge(node, config.nodeGroups.seedNode);
       } else if (isSelected) {
         // apply selected node style to node
-        console.log("node styles")
-        console.log(node)
-        console.log(config.nodeGroups.selectedNode)
         node = merge(node, config.nodeGroups.selectedNode);
       }
       // show image if image url is given
       if (node.image) {
         node.shape = 'image';
       }
-      // calculate color gradient if gradient is givel
+      // use opactiy as gradient
       if (gradient === null) {
-        node.color = NetworkSettings.Grey;
-      } else {
-        node.color = getGradientColor(NetworkSettings.White, node.color, gradient);
-      }
+          node.opacity = 0
+        } else {
+          node.opacity = gradient
+        }
       return node;
     }
 }
diff --git a/src/app/pages/explorer-page/explorer-page.component.html b/src/app/pages/explorer-page/explorer-page.component.html
index 468fc1c3..28978dcb 100644
--- a/src/app/pages/explorer-page/explorer-page.component.html
+++ b/src/app/pages/explorer-page/explorer-page.component.html
@@ -182,6 +182,11 @@
               </div>
             </ng-container>
         
+        <app-toggle class="footer-buttons" textOn="Drugs On" textOff="Off"
+            tooltipOn="Display adjacent drugs ON."
+            tooltipOff="Display adjacent drugs OFF."
+            [smallStyle]="smallStyle"
+            [value]="adjacentDrugs" (valueChange)="updateAdjacentDrugs($event)"></app-toggle>
 
         <app-toggle class="footer-buttons" textOn="Animation On" textOff="Off"
                     tooltipOn="Enable the network animation."
diff --git a/src/app/pages/explorer-page/explorer-page.component.ts b/src/app/pages/explorer-page/explorer-page.component.ts
index 6a844684..4d3ce811 100644
--- a/src/app/pages/explorer-page/explorer-page.component.ts
+++ b/src/app/pages/explorer-page/explorer-page.component.ts
@@ -11,7 +11,9 @@ import {
   Wrapper,
   getWrapperFromNode,
   Tissue,
-  ExpressionMap
+  ExpressionMap,
+  getDrugNodeId,
+  Drug
 } from '../../interfaces';
 import {ProteinNetwork} from '../../main-network';
 import {AnalysisService} from '../../services/analysis/analysis.service';
@@ -22,6 +24,7 @@ import {defaultConfig, EdgeGroup, IConfig, InteractionDatabase, NodeGroup} from
 import {NetexControllerService} from 'src/app/services/netex-controller/netex-controller.service';
 import {rgbaToHex, rgbToHex, standardize_color} from '../../utils'
 import * as merge from 'lodash/fp/merge'; 
+
 // import * as 'vis' from 'vis-network';
 // import {DataSet} from 'vis-data';
 // import {vis} from 'src/app/scripts/vis-network.min.js';
@@ -57,7 +60,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
     const configObj = JSON.parse(config);
     for (const key of Object.keys(configObj)) {
       if (key === 'nodeGroups') {
-        console.log("set node config")
         this.setConfigNodeGroup(key, configObj[key]);
         updateNetworkFlag = true;
         // dont set the key here, will be set in function
@@ -142,6 +144,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
 
   private dumpPositions = false;
   public physicsEnabled = false;
+  public adjacentDrugs = false;
+  public adjacentDrugList: Node[] = [];
+  public adjacentDrugEdgesList: Node[] = [];
 
   public queryItems: Wrapper[] = [];
   public showAnalysisDialog = false;
@@ -179,8 +184,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
     this.showDetails = false;
 
     this.analysis.subscribeList((items, selected) => {
-      console.log(selected)
-      console.log(items)
       if (!this.nodeData.nodes) {
         return;
       }
@@ -190,26 +193,26 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
         }
         const updatedNodes = [];
         for (const wrapper of items) {
-          const node: Node = this.nodeData.nodes.get(wrapper.id);
+          // const node: Node = this.nodeData.nodes.get(wrapper.id);
+          const node = wrapper.data as Node;
           if (!node) {
             continue;
           }
           const pos = this.networkInternal.getPositions([wrapper.id]);
           node.x = pos[wrapper.id].x;
           node.y = pos[wrapper.id].y;
-
+          console.log('before styling')
           const nodeStyled = NetworkSettings.getNodeStyle(
             node,
             this.myConfig,
             false,
             selected,
-            undefined,
-            undefined,
             1.0
             )
-          Object.assign(node, nodeStyled);
-
-          updatedNodes.push(node);
+          console.log('after styling')
+          nodeStyled.x = pos[wrapper.id].x;
+          nodeStyled.y = pos[wrapper.id].y;
+          updatedNodes.push(nodeStyled);
         }
         this.nodeData.nodes.update(updatedNodes);
       } else {
@@ -267,6 +270,17 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
     if (network.nodes.length) {
       network.nodes = await this.netex.mapNodes(network.nodes, this.myConfig.identifier);
     }
+    // use netexIds where posssible
+    const nodeIdMap = {};
+    network.nodes.forEach(node => {
+      nodeIdMap[node.id] = node.netexId ? node.netexId : node.id
+      node.id = nodeIdMap[node.id];
+    });
+    // adjust edge labels accordingly
+    network.edges.forEach(edge => {
+      edge.from = nodeIdMap[edge.from];
+      edge.to = nodeIdMap[edge.to];
+    });
     this.proteins = network.nodes;
     this.edges = network.edges;
   }
@@ -311,6 +325,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
   public async createNetwork() {
     this.analysis.resetSelection();
     this.selectedWrapper = null;
+    // getNetwork synchronizes the input network with the database
     await this.getNetwork();
     this.proteinData = new ProteinNetwork(this.proteins, this.edges);
 
@@ -394,7 +409,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
     }
   }
 
-  public updatePhysicsEnabled(bool) {
+  public updatePhysicsEnabled(bool: boolean) {
     this.physicsEnabled = bool;
     this.networkInternal.setOptions({
       physics: {
@@ -406,6 +421,30 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
     });
   }
 
+  public updateAdjacentDrugs(bool: boolean) {
+    this.adjacentDrugs = bool;
+    if (this.adjacentDrugs) {
+        this.netex.adjacentDrugs(this.myConfig.interactionDrugProtein, this.nodeData.nodes).subscribe(response => {
+          for (const interaction of response.pdis) {
+            const edge = {from: interaction.protein, to: interaction.drug};
+            this.adjacentDrugEdgesList.push(this.proteinData.mapCustomEdge(edge, this.myConfig));
+          }
+          for (const drug of response.drugs) {
+            drug.group = 'foundDrug';
+            drug.id = getDrugNodeId(drug)
+            this.adjacentDrugList.push(this.proteinData.mapCustomNode(drug, this.myConfig))
+          }
+          this.nodeData.nodes.add(this.adjacentDrugList);
+          this.nodeData.edges.add(this.adjacentDrugEdgesList);
+      })
+    } else {
+      this.nodeData.nodes.remove(this.adjacentDrugList);
+      this.nodeData.edges.remove(this.adjacentDrugEdgesList);
+      this.adjacentDrugList = [];
+      this.adjacentDrugEdgesList = [];
+    }
+  }
+
   /**
    * Function to set the node group attribute in config
    * Validates input NodeGroups and handles setting defaults
@@ -419,24 +458,32 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
       // stop if nodeGroups do not contain any information
       return;
     }
-    // // do not allow '_' in node Group names since it causes problems with backend
-    // nodeGroups = removeUnderscoreFromKeys(nodeGroups)
 
     // make sure all keys are set
     Object.entries(nodeGroups).forEach(([key, group]) => {
-      if (!('color' in group)) {
-        // use detailShowLabel default value if not set
-        group['color'] = defaultConfig.nodeGroups.default.color;
+      if (key in defaultConfig.nodeGroups) {
+        // skip the groups that overwrite default groups in case user only wants to overwrite partially
+        return
+      }
+      if (!group.color) {
+        throw `Group ${defaultConfig.nodeGroups.groupName} has no attribute 'color'.`;
+      }
+      if (!group.shape) {
+        throw `Group ${defaultConfig.nodeGroups.groupName} has no attribute 'shape'.`;
+      }
+      if (!group.groupName) {
+        throw `Group ${defaultConfig.nodeGroups.groupName} has no attribute 'groupName'.`;
       }
-      if (!('detailShowLabel' in group)) {
-        // use detailShowLabel default value if not set
-        group['detailShowLabel'] = defaultConfig.nodeGroups.default.detailShowLabel;
+      // set default values in case they are not set by user
+      // these values are not mandatory but are neede to override default vis js styles after e.g. deselecting
+      // because vis js "remembers" styles even though they are removed
+      if (!group.borderWidth) {
+        group.borderWidth = 0;
       }
-      if (!('font' in group)) {
-        // use detailShowLabel default value if not set
-        group['font'] = defaultConfig.nodeGroups.default.font;
+      if (!group.borderWidthSelected) {
+        group.borderWidthSelected = 0;
       }
-      // color needs to be hexacode to calculate gradient
+      // color needs to be hexacode to calculate gradient, group.color might not be set for seed and selected group
       if (!group.color.startsWith('#')) {
         // color is either rgba, rgb or string like "red"
         if (group.color.startsWith('rgba')) {
@@ -565,8 +612,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
             this.myConfig,
             false,
             this.analysis.inSelection(getWrapperFromNode(item)),
-            undefined,
-            undefined,
             1.0
             )
         )
@@ -605,8 +650,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
               this.myConfig,
               node.isSeed,
               this.analysis.inSelection(wrapper),
-              undefined,
-              undefined,
               gradient));
           // node.wrapper = wrapper;
           node.gradient = gradient;
diff --git a/src/app/services/netex-controller/netex-controller.service.ts b/src/app/services/netex-controller/netex-controller.service.ts
index f411d964..49772e96 100644
--- a/src/app/services/netex-controller/netex-controller.service.ts
+++ b/src/app/services/netex-controller/netex-controller.service.ts
@@ -4,6 +4,7 @@ import {HttpClient, HttpParams} from '@angular/common/http';
 import {AlgorithmType, QuickAlgorithmType} from '../analysis/analysis.service';
 import { Observable } from 'rxjs';
 import { Tissue, Node} from 'src/app/interfaces';
+import { InteractionDrugProteinDB } from 'src/app/config';
 
 @Injectable({
   providedIn: 'root'
@@ -93,4 +94,17 @@ export class NetexControllerService {
       .set('proteins', JSON.stringify(genesBackendIds));
     return this.http.get(`${environment.backend}tissue_expression/`, {params});
   }
+
+  public adjacentDrugs(pdiDataset: InteractionDrugProteinDB, nodes: Node[]): Observable<any> {
+    /**
+     * Returns the expression in the given tissue for given nodes and cancerNodes
+     */
+    // slice prefix of netex id away for direct lookup in db, if node not mapped to db, replace by undefined
+    const genesBackendIds = nodes.map( (node: Node) => node.netexId ? node.netexId.slice(1) : undefined);
+    const params = {
+      pdi_dataset: pdiDataset,
+      proteins: genesBackendIds
+    }
+    return this.http.post<any>(`${environment.backend}adjacent_drugs/`, params);
+  }
 }
-- 
GitLab