From 446732cf1fb1c2eceed710cf0508147c0ad55c50 Mon Sep 17 00:00:00 2001 From: Michael Hartung <michi@Michaels-MacBook-Pro.local> Date: Tue, 20 Jul 2021 19:12:56 +0200 Subject: [PATCH] edges in analysis network; handdling of node labels, replacing by chosen identifier if unset instead of netexId; handling of multiple ensg numbers for same node missing --- package-lock.json | 30 +++- .../analysis-panel.component.html | 13 +- .../analysis-panel.component.ts | 130 +++++++----------- .../network-legend.component.ts | 1 + src/app/interfaces.ts | 32 +---- src/app/main-network.ts | 116 ++++++++-------- .../explorer-page.component.html | 23 ++-- .../explorer-page/explorer-page.component.ts | 22 ++- 8 files changed, 173 insertions(+), 194 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4adaa340..590d8325 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3130,6 +3130,16 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -5498,6 +5508,13 @@ "escape-string-regexp": "^1.0.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -8277,6 +8294,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -14048,7 +14072,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/src/app/components/analysis-panel/analysis-panel.component.html b/src/app/components/analysis-panel/analysis-panel.component.html index a38369d2..313c541f 100644 --- a/src/app/components/analysis-panel/analysis-panel.component.html +++ b/src/app/components/analysis-panel/analysis-panel.component.html @@ -127,7 +127,7 @@ <div class="tab-content" *ngIf="task && task.info.done" [class.is-visible]="tab === 'network'"> <div class="card-image canvas-content" #networkWithLegend> <div *ngIf="myConfig.showLegend"> - <app-network-legend [config]="myConfig" [analysis]="false"></app-network-legend> + <app-network-legend [config]="myConfig" [analysis]="true"></app-network-legend> </div> <div class="fullheight center image1" #network> <button class="button is-loading center">Loading</button> @@ -180,7 +180,9 @@ <div class="dropdown-trigger"> <button (click)="expressionExpanded=!expressionExpanded" class="button is-rounded is-primary" [class.is-outlined]="!selectedTissue" - aria-haspopup="true" aria-controls="dropdown-menu" [ngClass]="{'button-small':smallStyle}"> + aria-haspopup="true" aria-controls="dropdown-menu" [ngClass]="{'button-small':smallStyle}" + pTooltip="Tissue expression data is provided by the GTEx project." tooltipPosition="top" + > <div [ngClass]="{'text-small':smallStyle}"> <span *ngIf="!selectedTissue">Tissue</span> <span *ngIf="selectedTissue">{{selectedTissue.name}}</span> @@ -212,9 +214,10 @@ </div> <app-toggle *ngIf="task.info.target === 'drug-target'" class="footer-buttons" textOn="Drugs On" textOff="Off" - tooltipOn="Display drugs in the network" tooltipOff="Hide drugs in the network" - [smallStyle]="smallStyle" - [value]="showDrugs" (valueChange)="toggleDrugs($event)"></app-toggle> + 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/components/analysis-panel/analysis-panel.component.ts b/src/app/components/analysis-panel/analysis-panel.component.ts index a62795cf..c51e633d 100644 --- a/src/app/components/analysis-panel/analysis-panel.component.ts +++ b/src/app/components/analysis-panel/analysis-panel.component.ts @@ -9,18 +9,15 @@ import { SimpleChanges, ViewChild, } from '@angular/core'; -import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import {environment} from '../../../environments/environment'; import {algorithmNames, AnalysisService} from '../../services/analysis/analysis.service'; import { Drug, EdgeType, - getNodeIdsFromPDI, - getNodeIdsFromPPI, + getDrugNodeId, getProteinNodeId, - getWrapperFromDrug, getWrapperFromNode, - getWrapperFromProtein, Node, Task, Tissue, @@ -31,6 +28,7 @@ import {toast} from 'bulma-toast'; import {NetworkSettings} from '../../network-settings'; import {NetexControllerService} from 'src/app/services/netex-controller/netex-controller.service'; import {defaultConfig, IConfig} from 'src/app/config'; +import { mapCustomEdge, mapCustomNode } from 'src/app/main-network'; declare var vis: any; @@ -85,6 +83,10 @@ export class AnalysisPanelComponent implements OnInit, OnChanges { public tab: 'meta' | 'network' | 'table' = 'table'; public physicsEnabled = true; + public adjacentDrugs = false; + public adjacentDrugList: Node[] = []; + public adjacentDrugEdgesList: Node[] = []; + private proteins: any; public effects: any; @@ -427,6 +429,12 @@ export class AnalysisPanelComponent implements OnInit, OnChanges { */ public createNetwork(result: any): { edges: any[], nodes: any[] } { const config = result.parameters.config; + this.myConfig = config; + + console.log(config) + console.log('config') + + const identifier = this.myConfig.identifier; // add drugGroup and foundNodesGroup for added nodes // these groups can be overwritten by the user @@ -443,30 +451,36 @@ export class AnalysisPanelComponent implements OnInit, OnChanges { const isSeed = attributes.isSeed || {}; const scores = attributes.scores || {}; const details = attributes.details || {}; - const wrappers: { [key: string]: Wrapper } = {}; for (const node of network.nodes) { // convert id to netex Id if exists const nodeDetails = details[node]; + + nodeDetails.id = nodeDetails.id ? nodeDetails.id : nodeDetails.netexId; if (nodeDetails.netexId && nodeDetails.netexId.startsWith('p')) { // node is protein from database, has been mapped on init to backend protein from backend // or was found during analysis + nodeDetails.group = nodeDetails.group ? nodeDetails.group : 'foundNode'; + nodeDetails.label = nodeDetails.label ? nodeDetails.label : nodeDetails[identifier]; this.proteins.push(nodeDetails); - wrappers[node] = getWrapperFromProtein(nodeDetails as Node); } else if (nodeDetails.netexId && nodeDetails.netexId.startsWith('d')) { // node is drug, was found during analysis - wrappers[node] = getWrapperFromDrug(nodeDetails as Drug); + nodeDetails.type = 'Drug'; + nodeDetails.group = 'foundDrug'; } else { // node is custom input from user, could not be mapped to backend protein - wrappers[node] = getWrapperFromNode(nodeDetails as Node); + nodeDetails.group = nodeDetails.group ? nodeDetails.group : 'default'; + nodeDetails.label = nodeDetails.label ? nodeDetails.label : nodeDetails[identifier] } // IMPORTANT we set seeds to "selected" and not to seeds. The user should be inspired to run // further analysis and the button function can be used to highlight seeds // option to use scores[node] as gradient, but sccores are very small - nodes.push(NetworkSettings.getNodeStyle(wrappers[node].data as Node, config, false, isSeed[node], 1)) + nodes.push(NetworkSettings.getNodeStyle(nodeDetails as Node, config, false, isSeed[node], 1)) } + console.log('nodes') + console.log(nodes) for (const edge of network.edges) { - edges.push(this.mapEdge(edge, this.inferEdgeGroup(edge), wrappers)); + edges.push(mapCustomEdge(edge, this.myConfig)); } return { nodes, @@ -474,80 +488,28 @@ export class AnalysisPanelComponent implements OnInit, OnChanges { }; } - private mapEdge(edge: any, type: 'protein-protein' | 'protein-drug', wrappers?: { [key: string]: Wrapper }): any { - let edgeColor; - if (type === 'protein-protein') { - edgeColor = { - color: NetworkSettings.getColor('edgeGeneGene'), - highlight: NetworkSettings.getColor('edgeGeneGeneHighlight'), - }; - const {from, to} = getNodeIdsFromPPI(edge, wrappers); - return { - from, to, - color: edgeColor, - }; - } else if (type === 'protein-drug') { - edgeColor = { - color: NetworkSettings.getColor('edgeHostDrug'), - highlight: NetworkSettings.getColor('edgeHostDrugHighlight'), - }; - const {from, to} = getNodeIdsFromPDI(edge); - return { - from, to, - color: edgeColor, - }; - } - } - - public async toggleDrugs(bool: boolean) { - this.showDrugs = bool; - this.nodeData.nodes.remove(this.drugNodes); - this.nodeData.edges.remove(this.drugEdges); - this.drugNodes = []; - this.drugEdges = []; - if (this.showDrugs) { - const result = await this.http.get<any>( - `${environment.backend}drug_interactions/?token=${this.token}`).toPromise().catch( - (err: HttpErrorResponse) => { - // simple logging, but you can do a lot more, see below - toast({ - message: 'An error occured while fetching the drugs.', - duration: 5000, - dismissible: true, - pauseOnHover: true, - type: 'is-danger', - position: 'top-center', - animate: {in: 'fadeIn', out: 'fadeOut'} - }); - this.showDrugs = false; - return; - }); - - const drugs = result.drugs; - const edges = result.edges; - - if (drugs.length === 0) { - toast({ - message: 'No drugs found.', - duration: 5000, - dismissible: true, - pauseOnHover: true, - type: 'is-warning', - position: 'top-center', - animate: {in: 'fadeIn', out: 'fadeOut'} - }); - } else { - // for (const drug of drugs) { - // this.drugNodes.push(this.mapNode(config, 'drug', drug, false, null)); - // } - for (const interaction of edges) { - const edge = {from: interaction.uniprotAc, to: interaction.drugId}; - this.drugEdges.push(this.mapEdge(edge, 'protein-drug')); - } - this.nodeData.nodes.add(Array.from(this.drugNodes.values())); - this.nodeData.edges.add(Array.from(this.drugEdges.values())); - } + 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(mapCustomEdge(edge, this.myConfig)); + } + for (const drug of response.drugs) { + drug.group = 'foundDrug'; + drug.id = getDrugNodeId(drug) + this.adjacentDrugList.push(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 = []; } } diff --git a/src/app/components/network-legend/network-legend.component.ts b/src/app/components/network-legend/network-legend.component.ts index 64984b1a..10bdb07d 100644 --- a/src/app/components/network-legend/network-legend.component.ts +++ b/src/app/components/network-legend/network-legend.component.ts @@ -13,6 +13,7 @@ export class NetworkLegendComponent implements OnInit { @Input() set config(value: IConfig) { // copy to not override user config value = JSON.parse(JSON.stringify(value)); + // remove selected node group since it is just a border delete value.nodeGroups.selectedNode; if (!this.analysis) { // do not show the analysis-groups in the explorer network diff --git a/src/app/interfaces.ts b/src/app/interfaces.ts index 69fcc9a3..f7c46aa3 100644 --- a/src/app/interfaces.ts +++ b/src/app/interfaces.ts @@ -110,10 +110,6 @@ export function getDrugNodeId(drug: Drug) { return drug.netexId } -export function getDrugBackendId(drug: Drug) { - return drug.netexId; -} - export function getNodeId(node: Node) { /** * Returns backend_id of Gene object @@ -130,7 +126,7 @@ export function getNetworkId(node: Node) { /** * Returns ID of a network node */ - return node.id + return node.netexId } export function getId(gene: Node) { @@ -140,19 +136,6 @@ export function getId(gene: Node) { return `${gene.id}`; } -export function getWrapperFromProtein(gene: Node): Wrapper { - /** - * Constructs wrapper interface for gene - */ - // if node does not have property group, it was found by the analysis - gene.group = gene.group ? gene.group : 'foundNode'; - gene.label = gene.label ? gene.label : gene.id - return { - id: getNetworkId(gene), - data: gene, - }; -} - export function getWrapperFromNode(gene: Node): Wrapper { /** * Constructs wrapper interface for gene @@ -161,22 +144,11 @@ export function getWrapperFromNode(gene: Node): Wrapper { gene.group = gene.group ? gene.group : 'default'; gene.label = gene.label ? gene.label : gene.id return { - id: getNetworkId(gene), + id: gene.id, data: gene, }; } - -export function getWrapperFromDrug(drug: Drug): Wrapper { - // set type and group - drug.type = 'Drug'; - drug.group = 'foundDrug'; - return { - id: getDrugNodeId(drug), - data: drug, - }; -} - export type EdgeType = 'protein-protein' | 'protein-drug'; export interface Wrapper { diff --git a/src/app/main-network.ts b/src/app/main-network.ts index 052d040b..c66def70 100644 --- a/src/app/main-network.ts +++ b/src/app/main-network.ts @@ -43,71 +43,16 @@ export class ProteinNetwork { }); } - /** Maps user input node to network node object - * If user input node has no group, fall back to default - * If user input node has group that is not defined, throw error - * - * @param customNode - * @param config - * @returns - */ - public mapCustomNode(customNode: any, config: IConfig): Node { - let node; - if (customNode.group === undefined) { - // fallback to default node - node = JSON.parse(JSON.stringify(defaultConfig.nodeGroups.default)); - node.group = 'default' - } else { - if (config.nodeGroups[customNode.group] === undefined) { - throw `Node with id ${customNode.id} has undefined node group ${customNode.group}.` - } - // copy - node = JSON.parse(JSON.stringify(config.nodeGroups[customNode.group])); - } - // update the node with custom node properties, including values fetched from backend - node = merge(node, customNode) - // label is only used for network visualization - node.label = customNode.label ? customNode.label : customNode.id; - return node; - } - - /** Maps user input edge to network edge object - * If user input edge has no group, fall back to default - * If user input edge has group that is not defined, throw error - * - * @param customEdge - * @param config - * @returns - */ - public mapCustomEdge(customEdge: NodeInteraction, config: IConfig): any { - let edge; - if (customEdge.group === undefined) { - // fallback to default node - edge = JSON.parse(JSON.stringify(defaultConfig.edgeGroups.default)); - } else { - if (config.edgeGroups[customEdge.group] === undefined) { - throw `Edge "from ${customEdge.from}" - "to ${customEdge.to}" has undefined edge group ${customEdge.group}.` - } - // copy - edge = JSON.parse(JSON.stringify(config.edgeGroups[customEdge.group])); - } - edge = { - ...edge, - ...customEdge - } - return edge; - } - public mapDataToNetworkInput(config: IConfig): { nodes: Node[], edges: any[]; } { const nodes = []; const edges = []; for (const protein of this.proteins) { - nodes.push(this.mapCustomNode(protein, config)); + nodes.push(mapCustomNode(protein, config)); } for (const edge of this.edges) { - edges.push(this.mapCustomEdge(edge, config)); + edges.push(mapCustomEdge(edge, config)); } return { @@ -117,3 +62,60 @@ export class ProteinNetwork { } } + +/** Maps user input node to network node object + * If user input node has no group, fall back to default + * If user input node has group that is not defined, throw error + * + * @param customNode + * @param config + * @returns + */ + export function mapCustomNode(customNode: any, config: IConfig): Node { + let node; + if (customNode.group === undefined) { + // fallback to default node + node = JSON.parse(JSON.stringify(defaultConfig.nodeGroups.default)); + node.group = 'default'; + } else { + if (config.nodeGroups[customNode.group] === undefined) { + throw `Node with id ${customNode.id} has undefined node group ${customNode.group}.` + } + // copy + node = JSON.parse(JSON.stringify(config.nodeGroups[customNode.group])); + } + // update the node with custom node properties, including values fetched from backend + node = merge(node, customNode) + // label is only used for network visualization + node.label = customNode.label ? customNode.label : customNode.id; + return node; +} + +/** Maps user input edge to network edge object + * If user input edge has no group, fall back to default + * If user input edge has group that is not defined, throw error + * + * @param customEdge + * @param config + * @returns + */ +export function mapCustomEdge(customEdge: NodeInteraction, config: IConfig): any { + let edge; + if (customEdge.group === undefined) { + // fallback to default node + edge = JSON.parse(JSON.stringify(defaultConfig.edgeGroups.default)); + } else { + if (config.edgeGroups[customEdge.group] === undefined) { + throw `Edge "from ${customEdge.from}" - "to ${customEdge.to}" has undefined edge group ${customEdge.group}.` + } + // copy + edge = JSON.parse(JSON.stringify(config.edgeGroups[customEdge.group])); + } + edge = { + ...edge, + ...customEdge + } + console.log('edge') + console.log(edge) + return edge; +} diff --git a/src/app/pages/explorer-page/explorer-page.component.html b/src/app/pages/explorer-page/explorer-page.component.html index 9ec8e526..87baa8c7 100644 --- a/src/app/pages/explorer-page/explorer-page.component.html +++ b/src/app/pages/explorer-page/explorer-page.component.html @@ -146,6 +146,7 @@ <ng-container *ngIf="myConfig.showFooterButtonExpression"> <div class="footer-buttons dropdown is-up explorer-footer-element" [class.is-active]="expressionExpanded"> + <div class="dropdown-trigger"> <button (click)="expressionExpanded=!expressionExpanded" class="button is-rounded is-primary" [class.is-outlined]="!selectedTissue" @@ -182,17 +183,17 @@ </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 explorer-footer-element" textOn="Animation On" textOff="Off" - tooltipOn="Enable the network animation." - tooltipOff="Disable the network animation and freeze nodes." - [smallStyle]="smallStyle" - [value]="physicsEnabled" (valueChange)="updatePhysicsEnabled($event)"></app-toggle> + <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 explorer-footer-element" textOn="Animation On" textOff="Off" + tooltipOn="Enable the network animation." + tooltipOff="Disable the network animation and freeze nodes." + [smallStyle]="smallStyle" + [value]="physicsEnabled" (valueChange)="updatePhysicsEnabled($event)"></app-toggle> </footer> </div> diff --git a/src/app/pages/explorer-page/explorer-page.component.ts b/src/app/pages/explorer-page/explorer-page.component.ts index cd2a64d4..c5d09b1a 100644 --- a/src/app/pages/explorer-page/explorer-page.component.ts +++ b/src/app/pages/explorer-page/explorer-page.component.ts @@ -15,7 +15,7 @@ import { getDrugNodeId, Drug } from '../../interfaces'; -import {ProteinNetwork} from '../../main-network'; +import {mapCustomEdge, mapCustomNode, ProteinNetwork} from '../../main-network'; import {AnalysisService} from '../../services/analysis/analysis.service'; import {OmnipathControllerService} from '../../services/omnipath-controller/omnipath-controller.service'; import domtoimage from 'dom-to-image'; @@ -269,10 +269,20 @@ 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 + // at this point, we have nodes synched with the backend + // use netexIds where posssible, but use original id as node name if no label given const nodeIdMap = {}; - network.nodes.forEach(node => { - nodeIdMap[node.id] = node.netexId ? node.netexId : node.id + const seenNodeIds = new Set(); + network.nodes.forEach((node, index, object) => { + if (seenNodeIds.has(node.id)) { + // remove duplicate ensg nodes, TODO is there a better way to do this? + object.splice(index, 1); + return; + } else { + seenNodeIds.add(node.id); + } + nodeIdMap[node.id] = node.netexId ? node.netexId : node.id; + node.label = node.label ? node.label : node.id; node.id = nodeIdMap[node.id]; }); // adjust edge labels accordingly @@ -425,12 +435,12 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { 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)); + this.adjacentDrugEdgesList.push(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.adjacentDrugList.push(mapCustomNode(drug, this.myConfig)) } this.nodeData.nodes.add(this.adjacentDrugList); this.nodeData.edges.add(this.adjacentDrugEdgesList); -- GitLab