import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, } from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {environment} from '../../../environments/environment'; import {algorithmNames, AnalysisService} from '../../services/analysis/analysis.service'; import { Drug, EdgeType, NodeAttributeMap, getDrugNodeId, getProteinNodeId, getWrapperFromNode, legendContext, Node, Task, Tissue, Wrapper, NodeInteraction, } from '../../interfaces'; import domtoimage from 'dom-to-image'; 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'; import { downLoadFile, removeDuplicateObjectsFromList } from 'src/app/utils'; declare var vis: any; interface Scored { score: number; // Normalized or unnormalized (whichever user selects, will be displayed in the table) rawScore: number; // Unnormalized (kept to restore unnormalized value) } interface Seeded { isSeed: boolean; } interface Baited { closestViralProteins: string[]; closestDistance: number; } @Component({ selector: 'app-analysis-panel', templateUrl: './analysis-panel.component.html', styleUrls: ['./analysis-panel.component.scss'], }) export class AnalysisPanelComponent implements OnInit, OnChanges { @ViewChild('network', {static: false}) networkEl: ElementRef; @ViewChild('networkWithLegend', {static: false}) networkWithLegendEl: ElementRef; @Input() token: string | null = null; @Input() public smallStyle = false; @Input() public set config(config: IConfig | undefined) { if (typeof config === 'undefined') { return; } for (const key of Object.keys(config)) { this.myConfig[key] = config[key]; } console.log(this.myConfig) } @Output() tokenChange = new EventEmitter<string | null>(); @Output() showDetailsChange = new EventEmitter<Wrapper>(); @Output() visibleItems = new EventEmitter<[any[], [Node[], Tissue], NodeInteraction[]]>(); public task: Task | null = null; public result: any = null; public myConfig: IConfig = JSON.parse(JSON.stringify(defaultConfig)); public network: any; private nodeData: { nodes: any, edges: any } = {nodes: null, edges: null}; private drugNodes: any[] = []; private drugEdges: any[] = []; public showDrugs = false; public tab: 'meta' | 'network' | 'table' = 'table'; public physicsEnabled = true; public adjacentDrugs = false; public adjacentDrugList: Node[] = []; public adjacentDrugEdgesList: Node[] = []; public highlightSeeds = false; public seedMap: NodeAttributeMap; private proteins: any; public effects: any; public tableDrugs: Array<Drug & Scored & Baited> = []; public tableProteins: Array<Node & Scored & Seeded & Baited> = []; public tableSelectedProteins: Array<Node & Scored & Seeded & Baited> = []; public tableViralProteins: Array<Scored & Seeded> = []; public tableSelectedViralProteins: Array<Scored & Seeded> = []; public tableNormalize = false; public tableHasScores = false; public legendContext: legendContext = 'drugTarget'; public expressionExpanded = false; public selectedTissue: Tissue | null = null; public algorithmNames = algorithmNames; public tableDrugScoreTooltip = ''; public tableProteinScoreTooltip = ''; public expressionMap: NodeAttributeMap; public gradientMap: NodeAttributeMap = {}; constructor(private http: HttpClient, public analysis: AnalysisService, public netex: NetexControllerService) { } async ngOnInit() { } async ngOnChanges(changes: SimpleChanges) { await this.refresh(); } private async refresh() { if (this.token) { this.task = await this.getTask(this.token); this.analysis.switchSelection(this.token); if (this.task.info.algorithm === 'degree') { this.tableDrugScoreTooltip = 'Normalized number of direct interactions of the drug with the seeds. ' + 'The higher the score, the more relevant the drug.'; this.tableProteinScoreTooltip = 'Normalized number of direct interactions of the protein with the seeds. ' + 'The higher the score, the more relevant the protein.'; } else if (this.task.info.algorithm === 'closeness' || this.task.info.algorithm === 'quick' || this.task.info.algorithm === 'super') { this.tableDrugScoreTooltip = 'Normalized inverse mean distance of the drug to the seeds. ' + 'The higher the score, the more relevant the drug.'; this.tableProteinScoreTooltip = 'Normalized inverse mean distance of the protein to the seeds. ' + 'The higher the score, the more relevant the protein.'; } else if (this.task.info.algorithm === 'trustrank') { this.tableDrugScoreTooltip = 'Amount of ‘trust’ on the drug at termination of the algorithm. ' + 'The higher the score, the more relevant the drug.'; this.tableProteinScoreTooltip = 'Amount of ‘trust’ on the protein at termination of the algorithm. ' + 'The higher the score, the more relevant the protein.'; } else if (this.task.info.algorithm === 'proximity') { this.tableDrugScoreTooltip = 'Empirical z-score of mean minimum distance between the drug’s targets and the seeds. ' + 'The lower the score, the more relevant the drug.'; this.tableProteinScoreTooltip = 'Empirical z-score of mean minimum distance between the drug’s targets and the seeds. ' + 'The lower the score, the more relevant the drug.'; } if (this.task && this.task.info.done) { this.result = await this.netex.getTaskResult(this.token); console.log(this.result) const nodeAttributes = this.result.nodeAttributes || {}; this.seedMap = nodeAttributes.isSeed || {}; // Reset this.nodeData = {nodes: null, edges: null}; this.networkEl.nativeElement.innerHTML = ''; this.network = null; this.showDrugs = false; // Create const {nodes, edges} = this.createNetwork(this.result); this.nodeData.nodes = new vis.DataSet(nodes); this.nodeData.edges = new vis.DataSet(edges); const container = this.networkEl.nativeElement; const isBig = nodes.length > 100 || edges.length > 100; const options = NetworkSettings.getOptions(isBig ? 'analysis-big' : 'analysis'); this.physicsEnabled = !isBig; this.network = new vis.Network(container, this.nodeData, options); this.tableDrugs = nodes.filter( e => e.netexId && e.netexId.startsWith('d'));; this.tableDrugs.forEach((r) => { r.rawScore = r.score; }); this.tableProteins = nodes.filter( e => e.netexId && e.netexId.startsWith('p')); this.tableSelectedProteins = []; this.tableProteins.forEach((r) => { r.rawScore = r.score; r.isSeed = this.seedMap[r.id]; const wrapper = getWrapperFromNode(r); if (this.analysis.inSelection(wrapper)) { this.tableSelectedProteins.push(r); } }); this.tableHasScores = ['trustrank', 'closeness', 'degree', 'proximity', 'betweenness', 'quick', 'super'] .indexOf(this.task.info.algorithm) !== -1; if (this.tableHasScores) { if (this.task.info.algorithm !== 'proximity') { this.toggleNormalization(true); } else { this.toggleNormalization(false); } } this.network.on('deselectNode', (properties) => { this.showDetailsChange.emit(null); }); this.network.on('doubleClick', (properties) => { const nodeIds: Array<string> = properties.nodes; if (nodeIds.length > 0) { const nodeId = nodeIds[0]; const node = this.nodeData.nodes.get(nodeId); if (node.nodeType === 'drug' || node.netexId === undefined || !node.netexId.startsWith('p')) { return; } const wrapper = getWrapperFromNode(node); if (this.analysis.inSelection(wrapper)) { this.analysis.removeItems([wrapper]); this.analysis.getCount(); } else { this.analysis.addItems([wrapper]); this.analysis.getCount(); } } }); this.network.on('click', (properties) => { const selectedNodes = this.nodeData.nodes.get(properties.nodes); if (selectedNodes.length > 0) { this.showDetailsChange.emit(getWrapperFromNode(selectedNodes[0])); } else { this.showDetailsChange.emit(null); } }); this.analysis.subscribeList((items, selected) => { // return if analysis panel is closed or no nodes are loaded if (!this.token) { return; } if (selected !== null) { const updatedNodes: Node[] = []; for (const item of items) { const node = this.nodeData.nodes.get(item.id); if (!node) { continue; } const pos = this.network.getPositions([item.id]); node.x = pos[item.id].x; node.y = pos[item.id].y; const isSeed = this.highlightSeeds ? this.seedMap[node.id] : false; const gradient = (this.gradientMap !== {}) && (this.gradientMap[item.id]) ? this.gradientMap[item.id] : 1.0; const nodeStyled = NetworkSettings.getNodeStyle( node, this.myConfig, isSeed, selected, gradient ) updatedNodes.push(nodeStyled); } this.nodeData.nodes.update(updatedNodes); const proteinSelection = this.tableSelectedProteins; const viralProteinSelection = this.tableSelectedViralProteins; for (const item of items) { // TODO: Refactor! const found = proteinSelection.findIndex((i) => getProteinNodeId(i) === item.id); const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.id); if (selected && found === -1 && tableItem) { proteinSelection.push(tableItem); } if (!selected && found !== -1 && tableItem) { proteinSelection.splice(found, 1); } } this.tableSelectedProteins = [...proteinSelection]; this.tableSelectedViralProteins = [...viralProteinSelection]; } else { // else: selected is null const updatedNodes = []; this.nodeData.nodes.forEach((node) => { // let drugType; // let drugInTrial; // if (node.netexId && node.netexId.startsWith('d')) { // drugType = node.status; // drugInTrial = node.inTrial; // } const isSeed = this.highlightSeeds ? this.seedMap[node.id] : false; const gradient = (this.gradientMap !== {}) && (this.gradientMap[node.id]) ? this.gradientMap[node.id] : 1.0; const nodeStyled = NetworkSettings.getNodeStyle( node, this.myConfig, isSeed, selected, gradient ) updatedNodes.push(nodeStyled); }); this.nodeData.nodes.update(updatedNodes); const proteinSelection = []; const viralProteinSelection = []; for (const item of items) { const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.id); if (tableItem) { proteinSelection.push(tableItem); } } this.tableSelectedProteins = [...proteinSelection]; this.tableSelectedViralProteins = [...viralProteinSelection]; } }); } } this.emitVisibleItems(true); this.setLegendContext(); } public emitVisibleItems(on: boolean) { if (on) { this.visibleItems.emit([this.nodeData.nodes, [this.proteins, this.selectedTissue], this.nodeData.edges]); } else { this.visibleItems.emit(null); } } private async getTask(token: string): Promise<any> { return await this.http.get(`${environment.backend}task/?token=${token}`).toPromise(); } close() { this.gradientMap = {}; this.expressionExpanded = false; this.expressionMap = undefined; this.seedMap = {}; this.highlightSeeds = false; this.showDrugs = false; this.analysis.switchSelection('main'); this.token = null; this.tokenChange.emit(this.token); this.emitVisibleItems(false); } public toggleNormalization(normalize: boolean) { this.tableNormalize = normalize; const normalizeFn = (table) => { let max = 0; table.forEach(i => { if (i.rawScore > max) { max = i.rawScore; } }); table.forEach(i => { i.score = i.rawScore / max; }); }; const unnormalizeFn = (table) => { table.forEach(i => { i.score = i.rawScore; }); }; if (normalize) { normalizeFn(this.tableDrugs); normalizeFn(this.tableProteins); normalizeFn(this.tableViralProteins); } else { unnormalizeFn(this.tableDrugs); unnormalizeFn(this.tableProteins); unnormalizeFn(this.tableViralProteins); } } public downloadLink(view: string): string { return `${environment.backend}task_result/?token=${this.token}&view=${view}&fmt=csv`; } public graphmlLink() { const data = {nodes: this.nodeData.nodes.get(), edges: this.nodeData.edges.get()} this.netex.graphmlLink(data).subscribe(response => { return downLoadFile(response, "application/xml"); }) } public inferEdgeGroup(edge: object): EdgeType { if (edge['to'].startsWith('d')) { return 'protein-drug'; } return 'protein-protein'; } /** * Infers wrapper type of node returned from backend. * Node can only be either an input node from the user with a defined group, * a drug found in the backend with either user defined type or default drug group, * or an intermediate protein added by the shortest path to the found drug. * For the third case, fall back to a default case which can also be set by user. */ public inferNodeGroup(wrapper: Wrapper): string { if (wrapper.data.group !== undefined) { return wrapper.data.group; } else if (wrapper.data.netexId !== undefined && wrapper.data.netexId.startsWith('d')) { return 'drug'; } else if (wrapper.data.netexId !== undefined && wrapper.data.netexId.startsWith('p')) { return 'protein'; } } public inferNodeLabel(config: IConfig, wrapper: Wrapper): string { if (wrapper.data.label) { return wrapper.data.label; } const identifier = config.identifier; if (identifier === 'uniprot') { return wrapper.data.uniprotAc; } else if (identifier === 'symbol') { return wrapper.data.symbol; } else if (identifier === 'ensg') { // heuristc to find most important ensg is to look for smallest id // parse ensg numbers to integers const ensg_numbers = wrapper.data.ensg.map(x => parseInt(x)); // get index of smalles number const i = ensg_numbers.reduce((iMin, x, i, arr) => x < arr[iMin] ? i : iMin, 0); // return ensg-ID return wrapper.data.ensg[i]; } } /** * Maps analysis result returned from database to valid Vis.js network input * * @param result * @returns */ public createNetwork(result: any): { edges: any[], nodes: any[] } { const config = result.parameters.config; this.myConfig = config; const identifier = this.myConfig.identifier; // add drugGroup and foundNodesGroup for added nodes // these groups can be overwritten by the user const nodes = []; const edges = []; const attributes = result.nodeAttributes || {}; this.proteins = []; this.effects = []; const network = result.network; const nodeTypes = attributes.nodeTypes || {}; const isSeed = attributes.isSeed || {}; const scores = attributes.scores || {}; const details = attributes.details || {}; 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); } else if (nodeDetails.netexId && nodeDetails.netexId.startsWith('d')) { // node is drug, was found during analysis nodeDetails.type = 'Drug'; nodeDetails.group = 'foundDrug'; } else { // node is custom input from user, could not be mapped to backend protein 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(nodeDetails as Node, config, false, false, 1)) } for (const edge of network.edges) { edges.push(mapCustomEdge(edge, this.myConfig)); } return { nodes, edges, }; } public setLegendContext() { const target = this.task.info.target; if (target === 'drug' || this.adjacentDrugs) { if (this.highlightSeeds) { this.legendContext = "drugAndSeeds"; } else { this.legendContext = "drug"; } } else if (target === 'drug-target') { if (this.highlightSeeds) { this.legendContext = "drugTargetAndSeeds"; } else { this.legendContext = 'drugTarget' } } else { throw `Could not set legend context based on ${target}.` } } public updateHighlightSeeds(bool: boolean) { this.highlightSeeds = bool; const updatedNodes = []; for (const item of this.proteins) { if (item.netexId === undefined) { // nodes that are not mapped to backend remain untouched continue; } const node: Node = this.nodeData.nodes.get(item.id); if (!node) { continue; } const pos = this.network.getPositions([item.id]); node.x = pos[item.id].x; node.y = pos[item.id].y; const isSeed = this.highlightSeeds ? this.seedMap[node.id] : false; const gradient = (this.gradientMap !== {}) && (this.gradientMap[item.id]) ? this.gradientMap[item.id] : 1.0; Object.assign( node, NetworkSettings.getNodeStyle( node, this.myConfig, isSeed, this.analysis.inSelection(getWrapperFromNode(item)), gradient ) ) updatedNodes.push(node); } this.nodeData.nodes.update(updatedNodes); this.setLegendContext(); } 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 = []; } this.setLegendContext() // emit data to update sidebar information this.emitVisibleItems(true); } public updatePhysicsEnabled(bool: boolean) { this.physicsEnabled = bool; this.network.setOptions({ physics: { enabled: this.physicsEnabled, stabilization: { enabled: false, }, } }); } public toImage() { this.downloadDom(this.networkWithLegendEl.nativeElement).catch(error => { console.error("Falling back to network only screenshot. Some components seem to be inaccessable, most likely the legend is a custom image with CORS access problems on the host server side.") this.downloadDom(this.networkEl.nativeElement).catch(e => { console.error("Some network content seems to be inaccessable for saving as a screenshot. This can happen due to custom images used as nodes. Please ensure correct CORS accessability on the images host server.") console.error(e) }); }); } public downloadDom(dom: object) { return domtoimage.toPng(dom, {bgcolor: '#ffffff'}).then((generatedImage) => { const a = document.createElement('a'); a.href = generatedImage; a.download = `Network.png`; a.click(); }); } public tableProteinSelection = (e): void => { const oldSelection = [...this.tableSelectedProteins]; this.tableSelectedProteins = e; const addItems = []; const removeItems = []; for (const i of this.tableSelectedProteins) { const wrapper = getWrapperFromNode(i); if (oldSelection.indexOf(i) === -1) { addItems.push(wrapper); } } for (const i of oldSelection) { const wrapper = getWrapperFromNode(i); if (this.tableSelectedProteins.indexOf(i) === -1) { removeItems.push(wrapper); } } this.analysis.addItems(addItems); this.analysis.removeItems(removeItems); } public previewStringArray(arr: string[], count: number): string { if (arr.length < count) { return arr.join(', '); } else { return arr.slice(0, count).join(', ') + `, ... (${arr.length})`; } } public selectTissue(tissue: Tissue | null) { this.expressionExpanded = false; if (!tissue) { this.selectedTissue = null; const updatedNodes = []; for (const item of this.proteins) { if (item.netexId === undefined) { // nodes that are not mapped to backend remain untouched continue; } const node: Node = this.nodeData.nodes.get(item.id); if (!node) { continue; } const pos = this.network.getPositions([item.id]); node.x = pos[item.id].x; node.y = pos[item.id].y; const isSeed = this.highlightSeeds ? this.seedMap[node.id] : false; Object.assign( node, NetworkSettings.getNodeStyle( node, this.myConfig, isSeed, this.analysis.inSelection(getWrapperFromNode(item)), 1.0 ) ) updatedNodes.push(node); } this.nodeData.nodes.update(updatedNodes); // delete expression values this.expressionMap = undefined; this.gradientMap = {}; } else { this.selectedTissue = tissue const minExp = 0.3; // filter out non-proteins, e.g. drugs const proteinNodes = []; this.nodeData.nodes.forEach(element => { if (element.id.startsWith('p') && element.netexId !== undefined) { proteinNodes.push(element); } }); this.netex.tissueExpressionGenes(this.selectedTissue, proteinNodes).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) this.gradientMap[netexId] = expressionlvl !== null ? (Math.pow(expressionlvl / maxExpr, 1 / 3) * (1 - minExp) + minExp) : -1; const pos = this.network.getPositions([networkId]); node.x = pos[networkId].x; node.y = pos[networkId].y; const isSeed = this.highlightSeeds ? this.seedMap[node.id] : false; Object.assign(node, NetworkSettings.getNodeStyle( node, this.myConfig, isSeed, this.analysis.inSelection(wrapper), this.gradientMap[netexId])); node.gradient = this.gradientMap[netexId]; updatedNodes.push(node); } this.nodeData.nodes.update(updatedNodes); }) } } }