diff --git a/src/app/components/analysis-panel/analysis-panel.component.ts b/src/app/components/analysis-panel/analysis-panel.component.ts index f8ee593867a9142855861edd17b2fac1f0c1ab2c..164b6a36dd0e75af484ad2dcadcf528c8aef8cfb 100644 --- a/src/app/components/analysis-panel/analysis-panel.component.ts +++ b/src/app/components/analysis-panel/analysis-panel.component.ts @@ -443,21 +443,19 @@ export class AnalysisPanelComponent implements OnInit, OnChanges { const wrappers: { [key: string]: Wrapper } = {}; for (const node of network.nodes) { - // backend converts object keys to PascalCase: p_123 --> p123 - const nodeObjectKey = node.split('_').join(''); - if (nodeTypes[nodeObjectKey] === 'protein') { + if (nodeTypes[node] === 'protein') { // node is protein from database, has been mapped on init to backend protein from backend // or was found during analysis - this.proteins.push(details[nodeObjectKey]); - wrappers[node] = getWrapperFromNode(details[nodeObjectKey]); - } else if (nodeTypes[nodeObjectKey] === 'drug') { + this.proteins.push(details[node]); + wrappers[node] = getWrapperFromNode(details[node]); + } else if (nodeTypes[node] === 'drug') { // node is drug, was found during analysis - wrappers[node] = getWrapperFromDrug(details[nodeObjectKey]); + wrappers[node] = getWrapperFromDrug(details[node]); } else { // node is custom input from user, could not be mapped to backend protein - wrappers[node] = getWrapperFromCustom(details[nodeObjectKey]); + wrappers[node] = getWrapperFromCustom(details[node]); } - nodes.push(this.mapNode(config, wrappers[node], isSeed[nodeObjectKey], scores[nodeObjectKey])); + nodes.push(this.mapNode(config, wrappers[node], isSeed[node], scores[node])); } for (const edge of network.edges) { edges.push(this.mapEdge(edge, this.inferEdgeGroup(edge), wrappers)); @@ -477,20 +475,17 @@ export class AnalysisPanelComponent implements OnInit, OnChanges { * @returns */ private mapNode(config: IConfig, wrapper: Wrapper, isSeed?: boolean, score?: number): any { - - console.log('node group'); - console.log(config.nodeGroups); - console.log('node'); - - console.log(wrapper.data); - // override group is node is seed - wrapper.data.group = isSeed ? 'seedNode' : wrapper.data.group; + // TODO Move this to extra function + // wrapper.data.group = isSeed ? 'seedNode' : wrapper.data.group; const node = JSON.parse(JSON.stringify(config.nodeGroups[wrapper.data.group])); node.id = wrapper.id; node.label = this.inferNodeLabel(config, wrapper); node.isSeed = isSeed; node.wrapper = wrapper; + if (node.image) { + node.shape = 'image'; + } return node; } diff --git a/src/app/config.ts b/src/app/config.ts index 09c462e1692f4f01fcb12bae6fc163f15d8d30f4..3dc540e6ca08f2ebf30dbd4a4fdf5b7cb84a94d3 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -36,6 +36,8 @@ export interface IConfig { showTasks: boolean; showSelection: boolean; showFooter: boolean; + showFooterButtonExpression: boolean; + showFooterButtonScreenshot: boolean; showLegend: boolean; showLegendNodes: boolean; showLegendEdges: boolean; @@ -69,6 +71,8 @@ export const defaultConfig: IConfig = { showTasks: true, showFooter: true, showLegend: true, + showFooterButtonExpression: true, + showFooterButtonScreenshot: true, identifier: 'symbol', interactionDrugProtein: 'DrugBank', interactionProteinProtein: 'STRING', @@ -78,7 +82,7 @@ export const defaultConfig: IConfig = { // 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: 'yellow', + color: '#FFFF00', shape: 'triangle', type: 'default type', detailShowLabel: false, diff --git a/src/app/interfaces.ts b/src/app/interfaces.ts index b067fe4801212aaa840d14956bf9082c8f27a3bf..818af38611b96a45f36dd29734143d4d806faa31 100644 --- a/src/app/interfaces.ts +++ b/src/app/interfaces.ts @@ -24,10 +24,9 @@ export interface Tissue { name: string; } -export interface ExpressionMap { - // node --> expression level - netexId: number; -} +/// netexId to expressionlvl +export type ExpressionMap = { string: number }; + export interface NodeInteraction { from: string; diff --git a/src/app/network-settings.ts b/src/app/network-settings.ts index b7e3794f9daa307cc198e360cfbc97a96380b554..b8ed0268f14e29c9e441ee7a6277c9c809f11f9e 100644 --- a/src/app/network-settings.ts +++ b/src/app/network-settings.ts @@ -2,7 +2,7 @@ import {getGradientColor} from './utils'; import { Node, } from './interfaces'; -import { IConfig } from './config'; +import { IConfig, defaultConfig} from './config'; export class NetworkSettings { @@ -183,17 +183,24 @@ export class NetworkSettings { drugType?: string, drugInTrial?: boolean, gradient?: number): any { - console.log(node) - if (!gradient) { - gradient = 1.0; + + let nodeGroupObject; + if (node.group === 'default') { + nodeGroupObject = defaultConfig.nodeGroups.default; + } else { + nodeGroupObject = config.nodeGroups[node.group]; + } + let nodeColor; + if (gradient === null) { + nodeColor = NetworkSettings.Grey; + } else { + nodeColor = getGradientColor(NetworkSettings.White, nodeGroupObject.color, gradient); } - const nodeGroupObject = config.nodeGroups[node.group]; // vis js style attributes const nodeShadow = true; // const nodeShape = node.shape; // const nodeSize = 10; // const nodeFont = node.font; - const nodeColor = nodeGroupObject.color; if (isSeed) { node.color = { background: nodeColor, diff --git a/src/app/pages/explorer-page/explorer-page.component.html b/src/app/pages/explorer-page/explorer-page.component.html index aa6b6173a8b48f4747008407073555a1ea13b6dd..468fc1c3ee5709a79ff3ba674112fa94a248e4e5 100644 --- a/src/app/pages/explorer-page/explorer-page.component.html +++ b/src/app/pages/explorer-page/explorer-page.component.html @@ -133,49 +133,55 @@ </div> <footer *ngIf="myConfig.showFooter" class="card-footer toolbar explorer-footer"> - <button (click)="toImage()" class="button is-primary is-rounded has-tooltip" - data-tooltip="Take a screenshot of the current network."> - <span class="icon"> - <i class="fas fa-camera" aria-hidden="true"></i> - </span> - <span [ngClass]="{'text-normal':smallStyle}">Screenshot</span> - </button> - <div class="footer-buttons dropdown is-up" [class.is-active]="expressionExpanded"> - <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" - data-tooltip="Tissue expression data is provided by the GTEx project." - [ngClass]="{'button-small':smallStyle}"> - <span *ngIf="!selectedTissue" [ngClass]="{'text-small':smallStyle}">Tissue</span> - <span *ngIf="selectedTissue">{{selectedTissue.name}}</span> - <span *ngIf="expressionExpanded" class="icon is-small"> - <i class="fas fa-angle-up" aria-hidden="true"></i> - </span> - <span *ngIf="!expressionExpanded" class="icon is-small"> - <i class="fas fa-angle-left" aria-hidden="true"></i> - </span> - </button> - </div> - <div class="dropdown-menu" id="dropdown-menu" role="menu"> - <div class="dropdown-content tissue-dropdown"> - <div class="scroll-area"> - <a (click)="selectTissue(null)" - [class.is-active]="!selectedTissue" - class="dropdown-item"> - None - </a> - <a *ngFor="let tissue of analysis.getTissues()" - (click)="selectTissue(tissue)" - [class.is-active]="selectedTissue && tissue.netexId === selectedTissue.netexId" - class="dropdown-item"> - {{tissue.name}} - </a> + <ng-container *ngIf="myConfig.showFooterButtonScreenshot"> + <button (click)="toImage()" class="button is-primary is-rounded has-tooltip" + data-tooltip="Take a screenshot of the current network."> + <span class="icon"> + <i class="fas fa-camera" aria-hidden="true"></i> + </span> + <span [ngClass]="{'text-normal':smallStyle}">Screenshot</span> + </button> + </ng-container> + + <ng-container *ngIf="myConfig.showFooterButtonExpression"> + <div class="footer-buttons dropdown is-up" [class.is-active]="expressionExpanded"> + <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" + data-tooltip="Tissue expression data is provided by the GTEx project." + [ngClass]="{'button-small':smallStyle}"> + <span *ngIf="!selectedTissue" [ngClass]="{'text-small':smallStyle}">Tissue</span> + <span *ngIf="selectedTissue">{{selectedTissue.name}}</span> + <span *ngIf="expressionExpanded" class="icon is-small"> + <i class="fas fa-angle-up" aria-hidden="true"></i> + </span> + <span *ngIf="!expressionExpanded" class="icon is-small"> + <i class="fas fa-angle-left" aria-hidden="true"></i> + </span> + </button> + </div> + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + <div class="dropdown-content tissue-dropdown"> + <div class="scroll-area"> + <a (click)="selectTissue(null)" + [class.is-active]="!selectedTissue" + class="dropdown-item"> + None + </a> + <a *ngFor="let tissue of analysis.getTissues()" + (click)="selectTissue(tissue)" + [class.is-active]="selectedTissue && tissue.netexId === selectedTissue.netexId" + class="dropdown-item"> + {{tissue.name}} + </a> + </div> + </div> + </div> </div> - </div> - </div> - </div> + </ng-container> + <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 98043505c14959bcf7be9e94edbc9e820103077d..c035e2a9db3f0f774a6fb0863c193c5d09ef13d9 100644 --- a/src/app/pages/explorer-page/explorer-page.component.ts +++ b/src/app/pages/explorer-page/explorer-page.component.ts @@ -20,6 +20,7 @@ import domtoimage from 'dom-to-image'; import {NetworkSettings} from '../../network-settings'; import {defaultConfig, EdgeGroup, IConfig, InteractionDatabase, NodeGroup} from '../../config'; import {NetexControllerService} from 'src/app/services/netex-controller/netex-controller.service'; +import {rgbaToHex, rgbToHex, standardize_color} from '../../utils' // import * as 'vis' from 'vis-network'; // import {DataSet} from 'vis-data'; // import {vis} from 'src/app/scripts/vis-network.min.js'; @@ -106,6 +107,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { return; } this.networkJSON = network; + console.log(this.myConfig) this.createNetwork(); } @@ -420,6 +422,19 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { // use detailShowLabel default value if not set group['detailShowLabel'] = defaultConfig.nodeGroups.default.detailShowLabel; } + // color needs to be hexacode to calculate gradient + if (!group.color.startsWith('#')) { + // color is either rgba, rgb or string like "red" + console.log(group.color) + if (group.color.startsWith('rgba')) { + group.color = rgbaToHex(group.color).slice(0, 7) + } else if (group.color.startsWith('rgb')) { + group.color = rgbToHex(group.color) + } else ( + group.color = standardize_color(group.color) + ) + console.log(group.color) + } }); // make sure that return-groups (seeds, drugs, found nodes) are set @@ -520,7 +535,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { this.selectedTissue = null; const updatedNodes = []; for (const item of this.proteins) { - console.log(item) if (item.netexId === undefined) { // nodes that are not mapped to backend remain untouched continue; @@ -529,8 +543,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { if (!node) { continue; } - console.log("node") - console.log(node) const pos = this.networkInternal.getPositions([item.id]); node.x = pos[item.id].x; node.y = pos[item.id].y; @@ -546,18 +558,53 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { 1.0 ) ) - console.log("in selection") - console.log(this.analysis.inSelection(getWrapperFromNode(item))) updatedNodes.push(node); } this.nodeData.nodes.update(updatedNodes); // delete expression values this.expressionMap = undefined; } else { - this.selectedTissue = tissue; - + this.selectedTissue = tissue const minExp = 0.3; + this.netex.tissueExpressionGenes(this.selectedTissue, this.nodeData.nodes).subscribe((response) => { + this.expressionMap = response; + const updatedNodes = []; + // mapping from netex IDs to network IDs, TODO check if this step is necessary + const networkIdMappping = {} + this.nodeData.nodes.forEach(element => { + networkIdMappping[element.netexId] = element.id + }); + const maxExpr = Math.max(...Object.values(this.expressionMap)); + for (const [netexId, expressionlvl] of Object.entries(this.expressionMap)) { + const networkId = networkIdMappping[netexId] + const node = this.nodeData.nodes.get(networkId); + if (node === null) { + continue; + } + const wrapper = getWrapperFromNode(node) + const gradient = expressionlvl !== null ? (Math.pow(expressionlvl / maxExpr, 1 / 3) * (1 - minExp) + minExp) : -1; + const pos = this.networkInternal.getPositions([networkId]); + node.x = pos[networkId].x; + node.y = pos[networkId].y; + Object.assign(node, + NetworkSettings.getNodeStyle( + node, + this.myConfig, + node.isSeed, + this.analysis.inSelection(wrapper), + undefined, + undefined, + gradient)); + // node.wrapper = wrapper; + node.gradient = gradient; + // this.proteins.find(prot => getProteinNodeId(prot) === netexId).expressionLevel = lvl.level; + // (node.wrapper.data as Node).expressionLevel = lvl.level; + updatedNodes.push(node); + } + this.nodeData.nodes.update(updatedNodes); + }) + // const params = new HttpParams().set('tissue', `${tissue.id}`).set('data', JSON.stringify(this.currentDataset)); // this.http.get<any>( // `${environment.backend}tissue_expression/`, {params}) diff --git a/src/app/services/analysis/analysis.service.ts b/src/app/services/analysis/analysis.service.ts index 3f8ae68e70fcec94731a2aa833534131d9213ad2..7bd72c48eb7c8081d316a049aa711eca454ef119 100644 --- a/src/app/services/analysis/analysis.service.ts +++ b/src/app/services/analysis/analysis.service.ts @@ -71,7 +71,7 @@ export class AnalysisService { } this.startWatching(); - this.http.get<Tissue[]>(`${environment.backend}tissues/`).subscribe((tissues) => { + this.netex.tissues().subscribe((tissues) => { this.tissues = tissues; }); } diff --git a/src/app/services/netex-controller/netex-controller.service.ts b/src/app/services/netex-controller/netex-controller.service.ts index ed486c51271dfe9f3ec75b91da51d2d9a635c066..a0a7287071d6f8f14f55a2f8044611833f6d00db 100644 --- a/src/app/services/netex-controller/netex-controller.service.ts +++ b/src/app/services/netex-controller/netex-controller.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import {environment} from '../../../environments/environment'; 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'; @Injectable({ providedIn: 'root' @@ -72,4 +74,26 @@ export class NetexControllerService { const payload = {nodes: JSON.stringify(nodes), identifier: JSON.stringify(identifier)}; return this.http.post(`${environment.backend}map_nodes/`, payload).toPromise(); } + + public tissues(): Observable<any> { + /** + * Lists all available tissues with id and name + */ + return this.http.get<Tissue[]>(`${environment.backend}tissues/`); + } + + public tissueExpressionGenes(tissue: Tissue, 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 + console.log("before genesBackendIds") + const genesBackendIds = nodes.map( (node: Node) => node.netexId ? node.netexId.slice(1) : undefined); + console.log("genesBackendIds") + console.log(genesBackendIds) + const params = new HttpParams() + .set('tissue', tissue.netexId) + .set('proteins', JSON.stringify(genesBackendIds)); + return this.http.get(`${environment.backend}tissue_expression/`, {params}); + } } diff --git a/src/app/utils.ts b/src/app/utils.ts index 6497399db514498e3281600d8dcc00b846fd1962..c2406510dd227dd95685d843e3bbfa1c60980b54 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -54,3 +54,53 @@ export function removeUnderscoreFromKeys(obj) { }); return result; } + +// https://gist.github.com/whitlockjc/9363016 +function trim (str) { + return str.replace(/^\s+|\s+$/gm,''); +} + +export function rgbaToHex (rgba) { + const inParts = rgba.substring(rgba.indexOf("(")).split(","), + r = parseInt(trim(inParts[0].substring(1)), 10), + g = parseInt(trim(inParts[1]), 10), + b = parseInt(trim(inParts[2]), 10), + a: number = parseFloat(parseFloat(trim(inParts[3].substring(0, inParts[3].length - 1))).toFixed(2)); + const outParts = [ + r.toString(16), + g.toString(16), + b.toString(16), + Math.round(a * 255).toString(16).substring(0, 2) + ]; + + // Pad single-digit output values + outParts.forEach(function (part, i) { + if (part.length === 1) { + outParts[i] = '0' + part; + } + }) + + return ('#' + outParts.join('')); +} + +// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb +function componentToHex(c) { + const hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; +} + +export function rgbToHex(rgb) { + const inParts = rgb.substring(rgb.indexOf("(")).split(","), + r = parseInt(trim(inParts[0].substring(1)), 10), + g = parseInt(trim(inParts[1]), 10), + b = parseInt(trim(inParts[2]), 10); + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); +} + +// https://stackoverflow.com/questions/1573053/javascript-function-to-convert-color-names-to-hex-codes/47355187#47355187 +export function standardize_color(str){ + var ctx = document.createElement("canvas").getContext("2d"); + ctx.fillStyle = str; + return ctx.fillStyle.toString(); +} + diff --git a/src/index.html b/src/index.html index 47884d34379d914d5cb74a722651d1838a75990e..8294d95a9c7b7e55a1f23107d2435af5a9197329 100644 --- a/src/index.html +++ b/src/index.html @@ -35,7 +35,7 @@ <network-expander id="netexp1" config='{ - "nodeGroups": {"0.5": {"type": "0.5er Instanz", "color": "rgb(204, 255, 51)", "groupName": "0.5", "shape": "circle"}, "patientgroup": {"type": "Patient", "detailShowLabel": "true", "color": "red", "groupName": "patient group", "shape": "dot", "size": "50"}, "pugGroup": {"type": "woof woof", "color": "grey", "groupName": "Pug Group", "shape": "triangle", "image": "https://static.raymondcamden.com/images/2016/11/pug.png"}}, + "nodeGroups": {"0.5": {"type": "0.5er Instanz", "color": "green", "groupName": "0.5", "shape": "hexagon"}, "patientgroup": {"type": "Patient", "detailShowLabel": "true", "color": "#632345", "groupName": "patient group", "shape": "dot", "size": "50"}, "pugGroup": {"type": "woof woof", "color": "grey", "groupName": "Pug Group", "shape": "triangle", "image": "https://static.raymondcamden.com/images/2016/11/pug.png"}}, "edgeGroups": {"dashes": {"color": "black", "groupName": "dashes Group", "dashes": [1, 2]}, "notdashes": {"color": "black", "groupName": "not dashes Group"}}, "identifier": "symbol" }'