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