From 572806964381db2ede7b9ef108bc71215190bc37 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov <dmitrii_krasnov@epam.com> Date: Tue, 1 Feb 2022 17:31:08 +0300 Subject: [PATCH] Metabolic pathways visualisation (#731): pathway tree search --- .../ngbCytoscapePathway.component.js | 2 +- .../ngbCytoscapePathway.controller.js | 74 +++++++++++++++++++ .../ngbCytoscapePathway.settings.js | 11 ++- .../ngbInternalPathwaysResult.controller.js | 11 ++- .../ngbInternalPathwaysResult.scss | 62 +++------------- .../ngbInternalPathwaysResult.tpl.html | 20 +++++ .../ngbInternalPathwaysTable.service.js | 3 +- 7 files changed, 129 insertions(+), 54 deletions(-) diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js index 862000c39..8690e2003 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js @@ -7,7 +7,7 @@ export default { tag: '@', onElementClick: '&', storageName: '@', - elementsOptions: '<' + searchParams: '<' }, controller: ngbCytoscapePathwayController }; diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index 3fd89dd93..38d2e55f6 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -6,6 +6,8 @@ const sbgnStylesheet = require('cytoscape-sbgn-stylesheet'); const $ = require('jquery'); const SCALE = 0.3; +const searchedColor = '#00cc00'; +let defaultNodeStyle = {}; export default class ngbCytoscapePathwayController { constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapeSettings) { @@ -57,6 +59,28 @@ export default class ngbCytoscapePathwayController { (changes.elements.previousValue.id !== changes.elements.currentValue.id)) { this.reloadCytoscape(true); } + if (!!changes.searchParams && + !!changes.searchParams.previousValue && + !!changes.searchParams.currentValue) { + if (changes.searchParams.currentValue.search + && changes.searchParams.previousValue.search !== changes.searchParams.currentValue.search) { + this.searchNode( + changes.searchParams.currentValue.search, + node => { + node.style({ + 'color': searchedColor, + 'border-color': searchedColor + }); + }, + node => { + node.style({ + 'color': defaultNodeStyle.color, + 'border-color': defaultNodeStyle['border-color'] + }); + } + ); + } + } } reloadCytoscape(active) { @@ -67,6 +91,10 @@ export default class ngbCytoscapePathwayController { } this.$timeout(() => { const sbgnStyle = sbgnStylesheet(Cytoscape); + defaultNodeStyle = { + ...this.settings.style.node, + ...this.getNodeStyle(sbgnStyle) + }; const savedState = JSON.parse(localStorage.getItem(this.storageName) || '{}'); const savedLayout = savedState.layout ? savedState.layout[this.elements.id] : undefined; let elements; @@ -210,4 +238,50 @@ export default class ngbCytoscapePathwayController { }); return nodes; } + + searchNode(term, onSatisfy, onDeny) { + if (!this.viewer) { + return; + } + const roots = this.viewer.nodes().roots(); + this.viewer.nodes().dfs({ + root: roots, + visit: node => { + if (this.deepSearch(node.data(), term)) { + onSatisfy(node); + } else { + onDeny(node); + } + } + }); + } + + deepSearch(obj, term) { + let result = false; + for (const key in obj) { + if (!obj.hasOwnProperty(key) || !obj[key]) continue; + if (obj[key] instanceof Object || obj[key] instanceof Array) { + result = this.deepSearch(obj[key], term); + } else { + result = obj[key].toString().toLocaleLowerCase() + .includes(term.toLocaleLowerCase()); + } + if (result) { + return true; + } + } + return false; + } + + getNodeStyle(style) { + const result = {}; + Object.keys(style).forEach(key => { + if (style[key].selector === 'node') { + Object.keys(style[key].properties).forEach(propKey => { + result[style[key].properties[propKey].name] = style[key].properties[propKey].value; + }); + } + }); + return result; + } } diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js index 2cc400569..4846ebd6d 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js @@ -1,6 +1,15 @@ export default { viewer: {}, - style: {}, + style: { + node: { + 'text-opacity': 1, + 'text-valign': 'center', + 'text-halign': 'center', + 'shape': 'rectangle', + 'color': '#000', + 'border-width': 1 + } + }, defaultLayout: { name: 'dagre', rankDir: 'TB' diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js index d9eaecb4a..533a3747f 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js @@ -2,7 +2,10 @@ import baseController from '../../../shared/baseController'; export default class ngbInternalPathwaysResultController extends baseController { selectedTree = null; - selectedTreeName = null; + treeSearchParams = { + search: null + }; + treeSearch = null; loading = true; treeError = false; @@ -61,6 +64,12 @@ export default class ngbInternalPathwaysResultController extends baseController this.$timeout(() => this.$scope.$apply()); } + searchInTree() { + this.treeSearchParams = { + search: this.treeSearch + }; + } + activePanelChanged(o) { const isActive = o === this.appLayout.Panels.pathways.panel; this.dispatcher.emit('cytoscape:panel:active', isActive); diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss index bf544dfca..98eb23aba 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -31,7 +31,7 @@ .tree-container { position: relative; - .element-description-container { + .pathway-search-panel { width: 50%; max-width: 300px; position: absolute; @@ -51,70 +51,32 @@ padding: 10px; box-shadow: 2px 2px 8px 2px #aaa; - .close { - cursor: pointer; - position: absolute; - top: 5px; - right: 5px; - fill: #777; - } - - .close:hover { - fill: #333333; - } - - .element-description-body { + .pathway-search-panel-body { display: table; padding-right: 16px; - .element-description-row { + .pathway-search-panel-row { display: flex; flex-wrap: wrap; align-items: baseline; border-spacing: 5px; word-break: break-all; padding: 5px 0; - - .element-description-title { - margin-right: 5px; - } - - .element-description-navigation { - font-weight: bold; - color: #2c4f9e; - text-decoration: underline; - cursor: pointer; - } - - .sequenced { - font-size: smaller; - font-style: italic; - white-space: nowrap; - } } - .element-description-attributes { - display: flex; - flex-direction: column; - align-items: flex-start; - - .element-description-attribute { - margin: 2px 0; - padding: 2px 4px; - background: #f9f9f9; - border: 1px solid #e9e9e9; - border-radius: 3px; - font-size: smaller; - text-transform: uppercase; - } + .pathway-tree-search-input { + font-size: 14px; + margin: 5px 0 10px 6px; } - .element-description-row:not(:last-child) { - border-bottom: 1px solid #eee; + .pathway-tree-search-input .md-errors-spacer { + min-height: 0; } - .element-description-empty { - color: #999; + .pathway-tree-search-button { + line-height: 32px; + min-height: 32px; + height: 32px; } } } diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index 170a8ea8b..1c210b733 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -16,6 +16,25 @@ <span class="blast-search-result-title">{{$ctrl.selectedTree.name}}</span> </div> <div class="u-height__full tree-container" flex layout="column"> + <div class="pathway-search-panel"> + <div class="md-content"> + <div class="pathway-search-panel-body" layout="column"> + <div class="pathway-search-panel-row"> + <form layout="row" ng-submit="$ctrl.searchInTree()"> + <md-input-container class="pathway-tree-search-input" flex> + <input id="pathwayTreeSearchKeyword" name="pathway_tree_search_keyword" + ng-model="$ctrl.treeSearch" type="text"> + </md-input-container> + <md-button aria-label="search node" + class="md-raised pathway-tree-search-button" + ng-click="$ctrl.searchInTree()"> + <ng-md-icon icon="search"></ng-md-icon> + </md-button> + </form> + </div> + </div> + </div> + </div> <md-content flex layout="column"> <div class="md-padding ngb-pathway-cytoscape-container"> <div class="internal-pathway-container-error" ng-if="$ctrl.treeError"> @@ -26,6 +45,7 @@ ng-if="!$ctrl.treeError" storage-name="{{$ctrl.ngbInternalPathwaysResultService.localStorageKey}}" tag="ngb-internal-pathway-node" + search-params="$ctrl.treeSearchParams" ></ngb-cytoscape-pathway> </div> </md-content> diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js index 2bb558d67..2e98f366c 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js @@ -67,7 +67,8 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer pageSize: this.pageSize, pageNum: this.currentPage }, - sortInfos: this.orderBy + sortInfos: this.orderBy, + term: currentSearch }; const data = await this.genomeDataService.getInternalPathwaysLoad(filter); if (data.error) {