From 2a628f9e1119d1557cf4dfd1bc4fab7af566150a Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Wed, 26 Jan 2022 20:05:38 +0300 Subject: [PATCH 01/14] Metabolic pathways visualisation (#731): internal pathways skeleton --- client/client/app/app.layout.constant.js | 8 + client/client/app/components/index.js | 4 +- .../app/components/ngbPathways/index.js | 29 +++ .../ngbPathways/ngbCytoscapePathway/index.js | 15 ++ .../ngbCytoscapePathway.component.js | 13 + .../ngbCytoscapePathway.controller.js | 229 ++++++++++++++++ .../ngbCytoscapePathway.scss | 10 + .../ngbCytoscapePathway.settings.js | 53 ++++ .../ngbCytoscapePathway.tpl.html | 4 + .../ngbInternalPathwaysResult/index.js | 17 ++ .../ngbInternalPathwaysResult.component.js | 9 + .../ngbInternalPathwaysResult.controller.js | 70 +++++ .../ngbInternalPathwaysResult.scss | 124 +++++++++ .../ngbInternalPathwaysResult.service.js | 47 ++++ .../ngbInternalPathwaysResult.tpl.html | 33 +++ .../ngbInternalPathwaysTable/index.js | 13 + .../ngbInternalPathwaysTable.component.js | 10 + .../ngbInternalPathwaysTable.controller.js | 204 +++++++++++++++ .../ngbInternalPathwaysTable.scss | 0 .../ngbInternalPathwaysTable.service.js | 245 ++++++++++++++++++ .../ngbInternalPathwaysTable.tpl.html | 27 ++ .../ngbPathways/ngbPathways.service.js | 53 ++++ .../ngbPathways/ngbPathwaysPanel.component.js | 6 + .../ngbPathwaysPanel.controller.js | 52 ++++ .../ngbPathways/ngbPathwaysPanel.html | 50 ++++ .../ngbPathways/ngbPathwaysPanel.scss | 22 ++ .../ngbPathwaysPanelPaginate/index.js | 13 + .../ngbPathwaysPanelPaginate.component.js | 11 + .../ngbPathwaysPanelPaginate.controller.js | 67 +++++ .../ngbPathwaysPanelPaginate.scss | 33 +++ .../ngbPathwaysPanelPaginate.tpl.html | 14 + .../ngbStrainLineage/ngbCytoscape/index.js | 2 +- .../ngbCytoscapeToolbarPanel/index.js | 0 .../ngbCytoscapeToolbarPanel.component.js | 0 .../ngbCytoscapeToolbarPanel.controller.js | 0 .../ngbCytoscapeToolbarPanel.scss | 0 .../ngbCytoscapeToolbarPanel.tpl.html | 9 +- .../genome/genome-data-service.js | 34 ++- client/package.json | 7 +- .../webpack.config.babel/webpack.loaders.js | 13 + 40 files changed, 1541 insertions(+), 9 deletions(-) create mode 100644 client/client/app/components/ngbPathways/index.js create mode 100644 client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js create mode 100644 client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js create mode 100644 client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.scss create mode 100644 client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js create mode 100644 client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.tpl.html create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysResult/index.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.component.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/index.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.component.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.scss create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html create mode 100644 client/client/app/components/ngbPathways/ngbPathways.service.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanel.component.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanel.html create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanel.scss create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/index.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.component.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.scss create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.tpl.html rename client/client/app/{components/ngbStrainLineage/ngbCytoscape => shared/components}/ngbCytoscapeToolbarPanel/index.js (100%) rename client/client/app/{components/ngbStrainLineage/ngbCytoscape => shared/components}/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.component.js (100%) rename client/client/app/{components/ngbStrainLineage/ngbCytoscape => shared/components}/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.controller.js (100%) rename client/client/app/{components/ngbStrainLineage/ngbCytoscape => shared/components}/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.scss (100%) rename client/client/app/{components/ngbStrainLineage/ngbCytoscape => shared/components}/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html (59%) diff --git a/client/client/app/app.layout.constant.js b/client/client/app/app.layout.constant.js index d201df22e..769f8faf8 100644 --- a/client/client/app/app.layout.constant.js +++ b/client/client/app/app.layout.constant.js @@ -86,6 +86,14 @@ export default { title: 'Lineage', name: 'layout>strainLineage' }, + pathways: { + displayed: false, + icon: 'device_hub', + panel: 'ngbPathwaysPanel', + position: 'right', + title: 'Pathways', + name: 'layout>pathways' + }, homologs: { isHidden: true, displayed: false, diff --git a/client/client/app/components/index.js b/client/client/app/components/index.js index 7c15039a5..b5a40f7f6 100644 --- a/client/client/app/components/index.js +++ b/client/client/app/components/index.js @@ -12,6 +12,7 @@ import ngbHomologsPanel from './ngbHomologsPanel'; import ngbLogModule from './ngbLog'; import ngbMolecularViewer from './ngbMolecularViewer'; import ngbOrganizeTracks from './ngbOrganizeTracks'; +import ngbPathways from './ngbPathways'; import ngbProjectInfoPanel from './ngbProjectInfoPanel'; import ngbSashimiPlot from './ngbSashimiPlot'; import ngbStrainLineage from './ngbStrainLineage'; @@ -40,5 +41,6 @@ export default angular.module('NGB_Panels', [ ngbGenesTablePanel, ngbHomologsPanel, ngbHeatmapPanel, - ngbStrainLineage + ngbStrainLineage, + ngbPathways ]).name; diff --git a/client/client/app/components/ngbPathways/index.js b/client/client/app/components/ngbPathways/index.js new file mode 100644 index 000000000..c7aec2d5d --- /dev/null +++ b/client/client/app/components/ngbPathways/index.js @@ -0,0 +1,29 @@ +import angular from 'angular'; +import cytoscapePathwayComponent from './ngbCytoscapePathway'; + + +// Import components +import ngbInternalPathwaysResult from './ngbInternalPathwaysResult'; +import ngbInternalPathwaysTable from './ngbInternalPathwaysTable'; +import service from './ngbPathways.service'; + +// Import internal modules +import ngbPathwaysPanel from './ngbPathwaysPanel.component'; +import controller from './ngbPathwaysPanel.controller'; + +// Import Style +import './ngbPathwaysPanel.scss'; +import ngbPathwaysPanelPaginate from './ngbPathwaysPanelPaginate'; + +// Import external modules +export default angular + .module('ngbPathwaysPanel', [ + cytoscapePathwayComponent, + ngbInternalPathwaysTable, + ngbInternalPathwaysResult, + ngbPathwaysPanelPaginate + ]) + .service('ngbPathwaysService', service.instance) + .component('ngbPathwaysPanel', ngbPathwaysPanel) + .controller(controller.UID, controller) + .name; diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js new file mode 100644 index 000000000..b2dc9ed73 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js @@ -0,0 +1,15 @@ +import angular from 'angular'; + +// Import internal modules +import cytoscapeComponent from './ngbCytoscapePathway.component'; +import cytoscapeController from './ngbCytoscapePathway.controller'; + +// Import Style +import './ngbCytoscapePathway.scss'; +import cytoscapeSettings from './ngbCytoscapePathway.settings'; + +export default angular.module('ngbCytoscapePathway', []) + .constant('cytoscapeSettings', cytoscapeSettings) + .controller(cytoscapeController.UID, cytoscapeController) + .component('ngbCytoscapePathway', cytoscapeComponent) + .name; diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js new file mode 100644 index 000000000..862000c39 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js @@ -0,0 +1,13 @@ +import ngbCytoscapePathwayController from './ngbCytoscapePathway.controller'; + +export default { + template: require('./ngbCytoscapePathway.tpl.html'), + bindings: { + elements: '<', + tag: '@', + onElementClick: '&', + storageName: '@', + elementsOptions: '<' + }, + controller: ngbCytoscapePathwayController +}; diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js new file mode 100644 index 000000000..f842bd961 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -0,0 +1,229 @@ +import angular from 'angular'; + +const Cytoscape = require('cytoscape'); +const graphml = require('cytoscape-graphml'); +const sbgnStylesheet = require('cytoscape-sbgn-stylesheet'); +const $ = require('jquery'); + +const SCALE = 0.3; +const elementOptionsType = { + NODE: 'nodes', + EDGE: 'edges' +}; + +export default class ngbCytoscapePathwayController { + constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapeSettings) { + this.$scope = $scope; + this.$compile = $compile; + this.cytoscapeContainer = $element.find('.cytoscape-container')[0]; + this.settings = cytoscapeSettings; + this.dispatcher = dispatcher; + this.$timeout = $timeout; + this.actionsManager = { + ready: false + }; + + const resizeHandler = () => { + if (this.resizeCytoscape()) { + this.centerCytoscape(); + } + }; + graphml(Cytoscape, $); + angular.element($window).on('resize', resizeHandler); + const cytoscapeActiveEventHandler = this.reloadCytoscape.bind(this); + this.dispatcher.on('cytoscape:panel:active', cytoscapeActiveEventHandler); + const handleSelectionChange = (e) => { + if (this.viewer && e && e.id) { + this.viewer.edges().forEach((edge) => { + const data = edge.data(); + if (data.id === e.id) { + // deselect logic + } + }); + } + }; + this.dispatcher.on('cytoscape:selection:change', handleSelectionChange); + $scope.$on('$destroy', () => { + this.dispatcher.removeListener('cytoscape:panel:active', cytoscapeActiveEventHandler); + this.dispatcher.removeListener('cytoscape:selection:change', handleSelectionChange); + angular.element($window).off('resize', resizeHandler); + }); + } + + static get UID() { + return 'ngbCytoscapePathwayController'; + } + + $onChanges(changes) { + if (!!changes.elements && + !!changes.elements.previousValue && + !!changes.elements.currentValue && + (changes.elements.previousValue.id !== changes.elements.currentValue.id)) { + this.reloadCytoscape(true); + } + } + + reloadCytoscape(active) { + if (active) { + if (this.viewer) { + this.viewer.destroy(); + this.viewer = null; + } + this.$timeout(() => { + const sbgnStyle = sbgnStylesheet(Cytoscape); + // Object.keys(sbgnStyle).forEach(key => { + // if(sbgnStyle[key].selector === 'edge') { + // Object.keys(sbgnStyle[key].properties).forEach(propKey => { + // if(sbgnStyle[key].properties[propKey].name === 'curve-style') { + // sbgnStyle[key].properties[propKey].value = 'taxi'; + // } + // }); + // } + // }); + // sbgnStyle.edge['curve-style'] = 'taxi'; + // const savedState = JSON.parse(localStorage.getItem(this.storageName) || '{}'); + // const savedLayout = savedState.layout ? savedState.layout[this.elements.id] : undefined; + // let elements, layoutSettings; + this.viewer = Cytoscape({ + container: this.cytoscapeContainer, + style: sbgnStyle, + layout: {name: 'preset'}, + elements: { + nodes: this.positionedNodes(this.elements.nodes), + edges: this.elements.edges, + }, + }); + const layout = this.viewer.layout(this.settings.loadedLayout); + layout.on('layoutready', () => { + this.$compile(this.cytoscapeContainer)(this.$scope); + this.viewer.on('dragfree', this.saveLayout.bind(this)); + this.resizeCytoscape(); + }); + this.viewer.edges().on('click', e => { + const edgeData = e.target.data(); + const { + label, + tooltip + } = edgeData || {}; + if (label || tooltip) { + this.onElementClick({ + data: { + id: edgeData.id, + tooltip: edgeData.tooltip, + title: edgeData.fullLabel + } + }); + } else { + this.onElementClick(null); + } + }); + layout.run(); + const viewerContext = this; + this.actionsManager = { + ZOOM_STEP: 0.25, + duration: 250, + zoom: () => viewerContext.viewer.zoom(), + zoomIn() { + const zoom = this.zoom() + this.ZOOM_STEP; + viewerContext.viewer.zoom(zoom); + viewerContext.centerCytoscape(); + this.canZoomIn = zoom < viewerContext.viewer.maxZoom(); + this.canZoomOut = zoom > viewerContext.viewer.minZoom(); + }, + zoomOut() { + const zoom = this.zoom() - this.ZOOM_STEP; + viewerContext.viewer.zoom(zoom); + viewerContext.centerCytoscape(); + this.canZoomIn = zoom < viewerContext.viewer.maxZoom(); + this.canZoomOut = zoom > viewerContext.viewer.minZoom(); + }, + canZoomIn: true, + canZoomOut: true, + ready: true + }; + }); + } + } + + resizeCytoscape() { + if (this.viewer) { + this.viewer.resize(); + const newSize = { + width: this.viewer.width(), + height: this.viewer.height() + }; + const changed = !this.viewerSize || + this.viewerSize.width !== newSize.width || + this.viewerSize.height !== newSize.height; + this.viewerSize = newSize; + return changed; + } + return false; + } + + centerCytoscape() { + if (this.viewer) { + this.viewer.center(); + } + } + + saveLayout() { + const savedState = JSON.parse(localStorage.getItem(this.storageName) || '{}'); + if (!Object.prototype.hasOwnProperty.call(savedState, 'layout')) { + savedState.layout = {}; + } + savedState.layout = { + ...savedState.layout, + [this.elements.id]: { + nodes: this.getPlainNodes(this.viewer.nodes().jsons()), + edges: this.viewer.edges().jsons() + } + }; + + localStorage.setItem(this.storageName, JSON.stringify(savedState)); + } + + getPlainNodes(nodes) { + return nodes.reduce((r, cv) => { + delete cv.data.dom; + r.push({ + data: cv.data, + position: cv.position, + selected: cv.selected + }); + return r; + }, []); + } + + applyOptions(elements, type) { + if (!this.elementsOptions) { + return; + } + switch (type) { + case elementOptionsType.NODE: { + return elements.reduce((r, cv) => { + if (this.elementsOptions.nodes[cv.data.id]) { + cv.data = { + ...cv.data, + ...this.elementsOptions.nodes[cv.data.id] + }; + } + r.push(cv); + return r; + }, []); + } + } + } + + positionedNodes(nodes) { + nodes.forEach(node => { + if (node.data.bbox) { + node.position = { + x: node.data.bbox.x/SCALE, + y: node.data.bbox.y/SCALE + }; + } + }); + return nodes; + } +} diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.scss b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.scss new file mode 100644 index 000000000..0f1204035 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.scss @@ -0,0 +1,10 @@ +ngb-cytoscape-pathway { + display: block; + height: 100%; + width: 100%; + position: relative; + .cytoscape-container { + width: 100%; + height: 100%; + } +} diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js new file mode 100644 index 000000000..4347a89de --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js @@ -0,0 +1,53 @@ +export default { + viewer: {}, + style: { + node: { + 'content': 'data(id)', + 'width': 175, + 'height': 60, + 'text-opacity': 1, + 'text-valign': 'center', + 'text-halign': 'center', + 'shape': 'rectangle', + 'label': 'data(id)', + 'background-opacity': 0, + 'opacity': 0, + 'color': '#000', + 'border-width': 1 + }, + edge: { + 'curve-style': 'bezier', + 'width': 1, + 'line-color': '#37474F', + 'target-arrow-color': '#37474F', + 'target-arrow-shape': 'triangle', + 'overlay-color': '#4285F4', + 'overlay-padding': '4px', + 'underlay-color': '#4285F4', + 'underlay-padding': '3px', + 'underlay-opacity': '0' + }, + edgeLabel: { + 'text-rotation': 'none', + 'content': 'data(label)', + 'font-size': '12px', + 'font-weight': 'bold', + 'text-background-color': '#fff', + 'text-background-opacity': 1, + 'color': '#2c4f9e' + } + }, + defaultLayout: { + name: 'dagre', + rankDir: 'TB' + }, + loadedLayout: { + name: 'preset', + nodeDimensionsIncludeLabels: true, + }, + options: { + wheelSensitivity: 0.25, + minZoom: 0.25, + maxZoom: 4 + } +}; diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.tpl.html b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.tpl.html new file mode 100644 index 000000000..8837dabae --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.tpl.html @@ -0,0 +1,4 @@ + +
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/index.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/index.js new file mode 100644 index 000000000..3751469a5 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/index.js @@ -0,0 +1,17 @@ +import angular from 'angular'; + +import component from './ngbInternalPathwaysResult.component'; + +// Import internal modules +import controller from './ngbInternalPathwaysResult.controller'; + +// Import Style +import './ngbInternalPathwaysResult.scss'; + +import service from './ngbInternalPathwaysResult.service'; + +export default angular.module('ngbInternalPathwaysResult', []) + .component('ngbInternalPathwaysResult', component) + .controller(controller.UID, controller) + .service('ngbInternalPathwaysResultService', service.instance) + .name; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.component.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.component.js new file mode 100644 index 000000000..1dc45591a --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.component.js @@ -0,0 +1,9 @@ +import controller from './ngbInternalPathwaysResult.controller'; + +export default { + bindings: { + changeState: '&' + }, + controller: controller.UID, + template: require('./ngbInternalPathwaysResult.tpl.html') +}; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js new file mode 100644 index 000000000..ab1af1b39 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js @@ -0,0 +1,70 @@ +import baseController from '../../../shared/baseController'; + +export default class ngbInternalPathwaysResultController extends baseController { + selectedTree = null; + selectedTreeName = null; + loading = true; + treeError = false; + + events = { + 'layout:active:panel:change': this.activePanelChanged.bind(this), + 'reference:change': this.initialize.bind(this), + 'reference:show:pathway': this.initialize.bind(this), + }; + + constructor( + $scope, + $timeout, + dispatcher, + ngbInternalPathwaysResultService, + ngbPathwaysService, + appLayout, + projectContext, + localDataService + ) { + super(); + + Object.assign( + this, + { + $scope, + $timeout, + dispatcher, + ngbInternalPathwaysResultService, + ngbPathwaysService, + appLayout, + projectContext, + localDataService + } + ); + + this.initialize(); + this.initEvents(); + } + + static get UID() { + return 'ngbInternalPathwaysResultController'; + } + + async initialize() { + if (!this.ngbPathwaysService.currentInternalPathwaysId) { + return; + } + const {data, error} = await this.ngbInternalPathwaysResultService.getPathwayTreeById(this.ngbPathwaysService.currentInternalPathwaysId); + if (error) { + this.treeError = error; + } else { + this.treeError = false; + this.selectedTree = data.tree; + this.selectedTreeName = data.name; + } + this.loading = false; + this.$timeout(() => this.$scope.$apply()); + } + + 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 new file mode 100644 index 000000000..149b7c7ea --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -0,0 +1,124 @@ +.ngb-cytoscape-container { + width: 100%; + height: 100%; + min-height: 500px; + flex: 1; +} + +.internal-pathway-container-error { + margin: 10px; + color: #999; +} + +.internal-pathway-container { + margin: 2px 10px; + height: calc(100% - 4px); + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.tree-list-divider { + height: 2px; + + & div { + width: 100%; + + md-divider { + border-top-color: #000; + } + } +} + +.tree-container { + position: relative; + + .element-description-container { + width: 50%; + max-width: 300px; + position: absolute; + top: 0; + right: 15px; + z-index: 1; + + .md-content { + display: flex; + flex-direction: column; + height: auto; + background: #FFFFFF; + color: #333333; + font-size: 13px; + border-radius: 5px; + border: 1px solid #aaa; + 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 { + display: table; + padding-right: 16px; + + .element-description-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; + } + } + + .element-description-row:not(:last-child) { + border-bottom: 1px solid #eee; + } + + .element-description-empty { + color: #999; + } + } + } + } +} diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js new file mode 100644 index 000000000..2edd80759 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js @@ -0,0 +1,47 @@ +export default class ngbInternalPathwaysResultService { + + currentReferenceId = null; + + constructor(genomeDataService, dispatcher) { + this.dispatcher = dispatcher; + this.genomeDataService = genomeDataService; + } + + static instance(genomeDataService, dispatcher) { + return new ngbInternalPathwaysResultService(genomeDataService, dispatcher); + } + + async getPathwayTreeById(id) { + if(!id) { + return { + data: null, + error: false + }; + } + const xml = require(`./xml/${id}.xml`); + try { + const convert = require('sbgnml-to-cytoscape'); + const data = convert(xml); + if (data) { + data.id = id; + return { + data: { + name: id, + tree: data + }, + error: false + }; + } else { + return { + data: null, + error: false + }; + } + } catch (e) { + return { + data: null, + error: e.message + }; + } + } +} diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html new file mode 100644 index 000000000..e4374fa4b --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -0,0 +1,33 @@ +
+
+ Loading... +
+ +
+
+
+ + + + {{$ctrl.selectedTreeName}} +
+
+ +
+
+ {{$ctrl.treeError}} +
+ +
+
+
+
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/index.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/index.js new file mode 100644 index 000000000..f2accbf01 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/index.js @@ -0,0 +1,13 @@ +// Import Style +import angular from 'angular'; +import component from './ngbInternalPathwaysTable.component'; +import controller from './ngbInternalPathwaysTable.controller'; +import './ngbInternalPathwaysTable.scss'; +import service from './ngbInternalPathwaysTable.service'; + +export default angular + .module('ngbInternalPathwaysTable', []) + .service('ngbInternalPathwaysTableService', service.instance) + .controller(controller.UID, controller) + .component('ngbInternalPathwaysTable', component) + .name; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.component.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.component.js new file mode 100644 index 000000000..226c15e30 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.component.js @@ -0,0 +1,10 @@ +import controller from './ngbInternalPathwaysTable.controller'; + +export default { + bindings: { + changeState: '&' + }, + controller: controller.UID, + restrict: 'E', + template: require('./ngbInternalPathwaysTable.tpl.html'), +}; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js new file mode 100644 index 000000000..419d052cb --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js @@ -0,0 +1,204 @@ +import baseController from '../../../shared/baseController'; +import {Debounce} from '../../../shared/utils/debounce'; + +const ROW_HEIGHT = 35; +const RESIZE_DELAY = 300; + +export default class ngbInternalPathwaysTableController extends baseController { + dispatcher; + isProgressShown = true; + isEmptyResult = false; + errorMessageList = []; + debounce = (new Debounce()).debounce; + gridOptions = { + enableSorting: false, + enableFiltering: false, + enableGridMenu: false, + enableHorizontalScrollbar: 0, + enablePinning: false, + enableRowHeaderSelection: false, + enableRowSelection: true, + headerRowHeight: 20, + height: '100%', + multiSelect: false, + rowHeight: ROW_HEIGHT, + showHeader: true, + treeRowHeaderAlwaysVisible: false, + saveWidths: true, + saveOrder: true, + saveScroll: false, + saveFocus: false, + saveVisible: true, + saveSort: false, + saveFilter: false, + savePinning: true, + saveGrouping: false, + saveGroupingExpandedStates: false, + saveTreeView: false, + saveSelection: false + }; + events = { + 'pathways:internalPathways:page:change': this.loadData.bind(this), + 'pathways:internalPathways:search': this.loadData.bind(this), + 'read:show:pathways': this.loadData.bind(this) + }; + + constructor($scope, $timeout, dispatcher, + ngbInternalPathwaysTableService, ngbPathwaysService, uiGridConstants) { + super(); + + Object.assign(this, { + $scope, + $timeout, + dispatcher, + ngbInternalPathwaysTableService, + ngbPathwaysService, + uiGridConstants, + }); + + this.initEvents(); + } + + static get UID() { + return 'ngbInternalPathwaysTableController'; + } + + $onInit() { + this.initialize(); + } + + async initialize() { + this.errorMessageList = []; + this.isProgressShown = true; + Object.assign(this.gridOptions, { + appScopeProvider: this.$scope, + columnDefs: this.ngbInternalPathwaysTableService.getInternalPathwaysGridColumns(), + onRegisterApi: (gridApi) => { + this.gridApi = gridApi; + this.gridApi.core.handleWindowResize(); + this.gridApi.selection.on.rowSelectionChanged(this.$scope, this.rowClick.bind(this)); + this.gridApi.colMovable.on.columnPositionChanged(this.$scope, this.saveColumnsState.bind(this)); + this.gridApi.colResizable.on.columnSizeChanged(this.$scope, this.saveColumnsState.bind(this)); + this.gridApi.core.on.sortChanged(this.$scope, this.sortChanged.bind(this)); + this.gridApi.core.on.gridDimensionChanged(this.$scope, this.debounce(this, this.onResize.bind(this), RESIZE_DELAY)); + this.gridApi.core.on.renderingComplete(this.$scope, gridApi => { + this.debounce(this, this.onResize.bind(this), RESIZE_DELAY)(0, 0, gridApi.grid.gridHeight); + }); + } + }); + await this.loadData(); + } + + async loadData() { + this.isProgressShown = true; + try { + await this.ngbInternalPathwaysTableService.searchInternalPathways(this.ngbPathwaysService.currentSearch); + const dataLength = this.ngbInternalPathwaysTableService.internalPathways.length; + if (this.ngbInternalPathwaysTableService.pageError) { + this.errorMessageList = [this.ngbInternalPathwaysTableService.pageError]; + this.gridOptions.data = []; + this.isEmptyResults = false; + } else if (dataLength) { + this.errorMessageList = []; + this.gridOptions.data = this.ngbInternalPathwaysTableService.internalPathways; + this.gridOptions.totalItems = dataLength; + this.isEmptyResults = false; + } else { + this.isEmptyResults = true; + } + this.isProgressShown = false; + } catch (errorObj) { + this.isProgressShown = false; + this.onError(errorObj.message); + } + this.$timeout(() => this.$scope.$apply()); + } + + onError(message) { + this.errorMessageList = [message]; + } + + rowClick(row, event) { + const entity = row.entity; + if (entity) { + this.ngbPathwaysService.currentInternalPathwaysId = row.entity.xml; + this.changeState({state: 'INTERNAL_PATHWAYS_RESULT'}); + } else { + event.stopImmediatePropagation(); + return false; + } + } + + saveColumnsState() { + if (!this.gridApi) { + return; + } + const {columns} = this.gridApi.saveState.save(); + const fieldTitleMap = ( + o => Object.keys(o).reduce( + (r, k) => Object.assign(r, { [o[k]]: k }), {} + ) + )(this.ngbInternalPathwaysTableService.columnTitleMap); + const mapNameToField = function ({name}) { + return fieldTitleMap[name]; + }; + const orders = columns.map(mapNameToField); + const r = []; + const names = this.ngbInternalPathwaysTableService.internalPathwaysColumns; + for (const name of names) { + r.push(orders.indexOf(name) >= 0); + } + let index = 0; + const result = []; + for (let i = 0; i < r.length; i++) { + if (r[i]) { + result.push(orders[index]); + index++; + } else { + result.push(names[i]); + } + } + this.ngbInternalPathwaysTableService.internalPathwaysColumns = result; + } + + sortChanged(grid, sortColumns) { + this.saveColumnsState(); + if (sortColumns && sortColumns.length > 0) { + this.ngbInternalPathwaysTableService.orderBy = sortColumns.map(sc => ({ + ascending: sc.sort.direction === 'asc', + field: this.ngbInternalPathwaysTableService.orderByColumns[sc.field] || sc.field + })); + } else { + this.ngbInternalPathwaysTableService.orderBy = null; + } + + this.ngbInternalPathwaysTableService.currentPage = 1; + this.gridOptions.data = []; + const sortingConfiguration = sortColumns + .filter(column => !!column.sort) + .map((column, priority) => ({ + field: column.field, + sort: ({ + ...column.sort, + priority + }) + })); + const {columns = []} = grid || {}; + columns.forEach(columnDef => { + const [sortingConfig] = sortingConfiguration + .filter(c => c.field === columnDef.field); + if (sortingConfig) { + columnDef.sort = sortingConfig.sort; + } + }); + this.loadData(); + } + + onResize(oldGridHeight, oldGridWidth, newGridHeight) { + const pageSize = Math.floor(newGridHeight / ROW_HEIGHT) - 2; + if (pageSize) { + this.ngbInternalPathwaysTableService.pageSize = pageSize; + this.loadData(); + } + } +} diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js new file mode 100644 index 000000000..cd7b91eaa --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js @@ -0,0 +1,245 @@ +import ClientPaginationService from '../../../shared/services/clientPaginationService'; +import {calculateColor} from '../../../shared/utils/calculateColor'; + +const DEFAULT_INTERNAL_PATHWAYS_COLUMNS = [ + 'name', 'description' +]; +const DEFAULT_ORDERBY_INTERNAL_PATHWAYS_COLUMNS = { + 'name': 'name', + 'description': 'description' +}; +const INTERNAL_PATHWAYS_COLUMN_TITLES = { + name: 'Name', + description: 'Description' +}; +const FIRST_PAGE = 1; +const PAGE_SIZE = 15; + +export default class ngbInternalPathwaysTableService extends ClientPaginationService { + + _internalPathwaysResult; + + constructor(dispatcher, genomeDataService) { + super(dispatcher, FIRST_PAGE, PAGE_SIZE, 'pathways:internalPathways:page:change'); + this.dispatcher = dispatcher; + this.genomeDataService = genomeDataService; + } + + _internalPathways; + + get internalPathways() { + return this._internalPathways; + } + + _pageError = null; + + get pageError() { + return this._pageError; + } + + get columnTitleMap() { + return INTERNAL_PATHWAYS_COLUMN_TITLES; + } + + get orderByColumns() { + return DEFAULT_ORDERBY_INTERNAL_PATHWAYS_COLUMNS; + } + + get internalPathwaysColumns() { + if (!localStorage.getItem('internalPathwaysColumns')) { + localStorage.setItem('internalPathwaysColumns', JSON.stringify(DEFAULT_INTERNAL_PATHWAYS_COLUMNS)); + } + return JSON.parse(localStorage.getItem('internalPathwaysColumns')); + } + + set internalPathwaysColumns(columns) { + localStorage.setItem('internalPathwaysColumns', JSON.stringify(columns || [])); + } + + static instance(dispatcher, genomeDataService) { + return new ngbInternalPathwaysTableService(dispatcher, genomeDataService); + } + + getInternalPathwaysResultById(id) { + return this._internalPathwaysResult[id]; + } + + getInternalPathwaysById(id) { + return this.internalPathways.filter(h => h.groupId === id)[0] || {}; + } + + async searchInternalPathways(currentSearch) { + const result = await this.loadInternalPathways(currentSearch); + this._internalPathways = result.internalPathways; + this._internalPathwaysResult = result.internalPathwaysResult; + this.dispatcher.emitSimpleEvent('internalPathways:result:change'); + } + + async loadInternalPathways(currentSearch) { + const emptyResult = { + internalPathways: [], + internalPathwaysResult: {} + }; + const filter = { + query: currentSearch, + page: this.currentPage, + pageSize: this.pageSize + }; + const data = await this.genomeDataService.getInternalPathwaysLoad(filter); + if (data.error) { + this.totalPages = 0; + this.currentPage = FIRST_PAGE; + this._firstPage = FIRST_PAGE; + this._pageError = data.message; + return emptyResult; + } else { + this._pageError = null; + } + this.totalPages = Math.ceil(data.totalCount / this.pageSize); + if (data && data.items) { + return { + internalPathways: this.getInternalPathwaysSearch(data.items), + internalPathwaysResult: this.getInternalPathwaysResult(data.items) + }; + } else { + return emptyResult; + } + } + + getInternalPathwaysSearch(data) { + const result = []; + data.forEach(value => result.push(this._formatServerToClient(value))); + return result; + } + + getInternalPathwaysResult(data) { + return data; + let maxHomologLength = 0; + const result = {}; + if (data) { + data.forEach(internalPathways => { + maxHomologLength = 0; + result[internalPathways.groupId] = []; + internalPathways.genes.forEach((gene, key) => { + result[internalPathways.groupId][key] = this._formatResultToClient(gene); + if (maxHomologLength < result[internalPathways.groupId][key].aa) { + maxHomologLength = result[internalPathways.groupId][key].aa; + } + }); + result[internalPathways.groupId].forEach((value, key) => { + result[internalPathways.groupId][key].domainsObj = { + domains: value.domains.map(d => ({...d, color: calculateColor(d.name)})), + homologLength: value.aa, + maxHomologLength: maxHomologLength, + accession_id: value.accession_id + }; + delete result[internalPathways.groupId][key].domains; + }); + }); + } + return result; + } + + getInternalPathwaysGridColumns() { + const result = []; + const columnsList = this.internalPathwaysColumns; + for (let i = 0; i < columnsList.length; i++) { + let sortDirection = 0; + let sortingPriority = 0; + let columnSettings = null; + const column = columnsList[i]; + if (this.orderBy) { + const fieldName = (DEFAULT_ORDERBY_INTERNAL_PATHWAYS_COLUMNS[column] || column); + const [columnSortingConfiguration] = this.orderBy.filter(o => o.field === fieldName); + if (columnSortingConfiguration) { + sortingPriority = this.orderBy.indexOf(columnSortingConfiguration); + sortDirection = columnSortingConfiguration.ascending ? 'asc' : 'desc'; + } + } + switch (column) { + case 'name': { + columnSettings = { + cellTemplate: ``, + enableHiding: false, + enableColumnMenu: false, + field: 'name', + name: this.columnTitleMap[column] + }; + break; + } + default: { + columnSettings = { + enableHiding: false, + enableColumnMenu: false, + field: column, + minWidth: 40, + name: this.columnTitleMap[column], + width: '*' + }; + break; + } + } + if (columnSettings) { + if (sortDirection) { + columnSettings.sort = { + direction: sortDirection, + priority: sortingPriority + }; + } + result.push(columnSettings); + } + } + return result; + } + + _formatServerToClient(internalPathways) { + return internalPathways; + const gene = new Set(); + const proteinFrequency = {}; + internalPathways.genes.forEach(g => { + gene.add(g.symbol); + if (proteinFrequency.hasOwnProperty(g.title)) { + proteinFrequency[g.title] += 1; + } else { + proteinFrequency[g.title] = 1; + } + }); + + const sortableProteinFrequency = []; + for (const protein in proteinFrequency) { + if (proteinFrequency.hasOwnProperty(protein)) { + sortableProteinFrequency.push([protein, proteinFrequency[protein]]); + } + } + + sortableProteinFrequency.sort((b, a) => a[1] - b[1]); + + return { + groupId: internalPathways.groupId, + gene: [...gene].sort().join(', '), + protein: sortableProteinFrequency[0] ? sortableProteinFrequency[0][0] : '', + info: internalPathways.caption + }; + } + + _formatResultToClient(result) { + return { + geneId: result.geneId, + name: result.symbol, + species: result.speciesScientificName, + accession_id: result.protAcc, + protGi: result.protGi, + aa: result.protLen, + taxId: result.taxId, + protein: result.title, + domains: (result.domains || []).map(d => ({ + id: d.pssmId, + start: d.begin, + end: d.end, + name: d.cddName + })) + }; + } + +} diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html new file mode 100644 index 000000000..ba5bef8e8 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html @@ -0,0 +1,27 @@ +
+
+ Loading pathways... +
+ +
+
+ Nothing found. Try to search another gene. +
+
+
+ {{$ctrl.historyLoadError}} +
+
+
+ +
+ +
diff --git a/client/client/app/components/ngbPathways/ngbPathways.service.js b/client/client/app/components/ngbPathways/ngbPathways.service.js new file mode 100644 index 000000000..1004ccf1f --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathways.service.js @@ -0,0 +1,53 @@ +const PATHWAYS_STATES = { + KEGG: 'KEGG', + INTERNAL_PATHWAYS: 'INTERNAL_PATHWAYS', + INTERNAL_PATHWAYS_RESULT: 'INTERNAL_PATHWAYS_RESULT' +}; + +export default class ngbPathwaysService { + pathwaysServiceMap = {}; + currentInternalPathwaysId; + + constructor(dispatcher, projectContext, + ngbInternalPathwaysTableService, ngbInternalPathwaysResultService + ) { + Object.assign( + this, + { + dispatcher, + projectContext + } + ); + this.pathwaysServiceMap = { + [PATHWAYS_STATES.INTERNAL_PATHWAYS]: ngbInternalPathwaysTableService, + [PATHWAYS_STATES.INTERNAL_PATHWAYS_RESULT]: ngbInternalPathwaysResultService, + }; + this.initEvents(); + } + + _currentSearch; + + get currentSearch() { + return this._currentSearch; + } + + set currentSearch(value) { + this._currentSearch = value; + } + + get pathwaysStates() { + return PATHWAYS_STATES; + } + + static instance(dispatcher, projectContext, + ngbInternalPathwaysTableService, ngbInternalPathwaysResultService) { + return new ngbPathwaysService(dispatcher, projectContext, + ngbInternalPathwaysTableService, ngbInternalPathwaysResultService); + } + + initEvents() { + this.dispatcher.on('read:show:pathways', data => { + this.currentSearch = data; + }); + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.component.js b/client/client/app/components/ngbPathways/ngbPathwaysPanel.component.js new file mode 100644 index 000000000..889dc4283 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.component.js @@ -0,0 +1,6 @@ +import controller from './ngbPathwaysPanel.controller'; + +export default { + template: require('./ngbPathwaysPanel.html'), + controller: controller.UID +}; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js new file mode 100644 index 000000000..e01bb83e5 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js @@ -0,0 +1,52 @@ +import baseController from '../../shared/baseController'; + +export default class ngbPathwaysPanelController extends baseController { + + pathwaysStates; + currentPathwaysState; + tabSelected; + searchRequest; + + events = { + 'read:show:pathways': () => { + this.changeState('INTERNAL_PATHWAYS'); + } + }; + + constructor(dispatcher, $scope, $timeout, ngbPathwaysService) { + super(dispatcher); + Object.assign(this, { + dispatcher, + $scope, + $timeout, + ngbPathwaysService + }); + this.pathwaysStates = this.ngbPathwaysService.pathwaysStates; + this.initEvents(); + } + + static get UID() { + return 'ngbPathwaysPanelController'; + } + + changeState(state) { + if (this.pathwaysStates.hasOwnProperty(state)) { + this.currentPathwaysState = this.pathwaysStates[state]; + this.service = this.ngbPathwaysService.pathwaysServiceMap[this.currentPathwaysState]; + switch (state) { + case this.pathwaysStates.INTERNAL_PATHWAYS: + case this.pathwaysStates.INTERNAL_PATHWAYS_RESULT: { + this.tabSelected = this.pathwaysStates.INTERNAL_PATHWAYS; + break; + } + } + } + this.$timeout(() => this.$scope.$apply()); + } + + searchPathway() { + this.ngbPathwaysService.currentSearch = this.searchRequest; + this.dispatcher.emitSimpleEvent('pathways:internalPathways:search'); + this.changeState('INTERNAL_PATHWAYS'); + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.html b/client/client/app/components/ngbPathways/ngbPathwaysPanel.html new file mode 100644 index 000000000..c57b444e4 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.html @@ -0,0 +1,50 @@ +
+ + + + + + Search + + + + + PATHWAYS + + + KEGG + + + + + + +
+ Type gene or feature to start search. +
+ + + + +
+
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss new file mode 100644 index 000000000..9e3695cef --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss @@ -0,0 +1,22 @@ +.pathways-link { + color: rgb(51, 103, 214); + font-weight: bold; + cursor: pointer; + a { + color: inherit; + text-decoration: none; + } +} +.pathways-search-result { + padding: 5px 2px; + + .pathways-search-result-title { + font-weight: bold; + display: inline-block; + line-height: 28px; + font-size: 18px; + } +} +.pathways-no-feature { + font-weight: bold; +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/index.js b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/index.js new file mode 100644 index 000000000..7a1ddc355 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/index.js @@ -0,0 +1,13 @@ +import angular from 'angular'; + +// Import internal modules +import component from './ngbPathwaysPanelPaginate.component'; +import controller from './ngbPathwaysPanelPaginate.controller'; +import './ngbPathwaysPanelPaginate.scss'; + + +// Import external modules +export default angular.module('ngbPathwaysPanelPaginate', []) + .controller(controller.UID, controller) + .component('ngbPathwaysPanelPaginate', component) + .name; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.component.js b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.component.js new file mode 100644 index 000000000..1b4055148 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.component.js @@ -0,0 +1,11 @@ +import controller from './ngbPathwaysPanelPaginate.controller'; + +export default { + bindings: { + totalPages: '<', + currentPage: '<', + changePage: '&' + }, + controller: controller.UID, + template: require('./ngbPathwaysPanelPaginate.tpl.html') +}; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.controller.js new file mode 100644 index 000000000..8c003ba41 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.controller.js @@ -0,0 +1,67 @@ +const PAGE_DEEPNESS = 3; +export default class ngbPathwaysPanelPaginate { + + constructor() { + } + + static get UID() { + return 'ngbPathwaysPanelPaginate'; + } + + $onChanges(changes) { + if (!!changes.totalPages && (changes.totalPages.previousValue !== changes.totalPages.currentValue)) { + this.setTotalPages(changes.totalPages.currentValue); + } + if (!!changes.currentPage && (changes.currentPage.previousValue !== changes.currentPage.currentValue)) { + this.setPage(changes.currentPage.currentValue); + } + } + + setTotalPages(totalPages) { + this.totalPages = totalPages; + this.pages = this.getPages(); + } + + setPage(page) { + this.currentPage = page; + this.pages = this.getPages(); + this.changePage({page: this.currentPage}); + } + + getPages() { + const totalPages = this.totalPages; + const currentPage = this.currentPage; + if (totalPages === undefined || currentPage === undefined) { + return []; + } + + let minimumPage = Math.max(1, currentPage - PAGE_DEEPNESS); + let maximumPage = Math.min(totalPages, currentPage + PAGE_DEEPNESS); + minimumPage = Math.max(1, Math.min(minimumPage, maximumPage - PAGE_DEEPNESS*2)); + maximumPage = Math.min(Math.max(maximumPage, minimumPage + PAGE_DEEPNESS*2), totalPages); + + const pages = []; + for (let i = minimumPage; i <= maximumPage; i++) { + if (i === minimumPage && minimumPage > 1) { + pages.push({ + isFirst: true, + isLast: false, + value: 1 + }); + } else if (i === maximumPage && maximumPage < totalPages) { + pages.push({ + isFirst: false, + isLast: true, + value: totalPages + }); + } else { + pages.push({ + isFirst: false, + isLast: false, + value: i + }); + } + } + return pages; + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.scss new file mode 100644 index 000000000..0b5776e9c --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.scss @@ -0,0 +1,33 @@ +ngb-pathways-panel-paginate { + font-size: 14px; + align-self: center; + height: auto; + .pathways-pagination { + margin-right: 5px; + display: flex; + flex-direction: row; + align-self: center; + justify-content: flex-end; + } + + .pathways-pagination-item { + float: left; + min-width: 10px; + min-height: 24px; + height: 24px; + padding: 0; + margin: 0 2px; + line-height: 24px; + color: rgb(51, 103, 214); + ng-md-icon { + fill: black; + min-height: 24px; + height: 24px; + } + &.active { + color: black; + text-decoration: underline; + font-weight: bold; + } + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.tpl.html new file mode 100644 index 000000000..a35e0e6ef --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanelPaginate/ngbPathwaysPanelPaginate.tpl.html @@ -0,0 +1,14 @@ +
+ + + {{page.value}} + + +
diff --git a/client/client/app/components/ngbStrainLineage/ngbCytoscape/index.js b/client/client/app/components/ngbStrainLineage/ngbCytoscape/index.js index 65f00a37e..a4509c943 100644 --- a/client/client/app/components/ngbStrainLineage/ngbCytoscape/index.js +++ b/client/client/app/components/ngbStrainLineage/ngbCytoscape/index.js @@ -1,4 +1,5 @@ import angular from 'angular'; +import ngbCytoscapeToolbarPanelModule from '../../../shared/components/ngbCytoscapeToolbarPanel'; // Import internal modules import cytoscapeComponent from './ngbCytoscape.component'; @@ -7,7 +8,6 @@ import cytoscapeController from './ngbCytoscape.controller'; // Import Style import './ngbCytoscape.scss'; import cytoscapeSettings from './ngbCytoscape.settings'; -import ngbCytoscapeToolbarPanelModule from './ngbCytoscapeToolbarPanel'; export default angular.module('ngbCytoscape', [ngbCytoscapeToolbarPanelModule]) .constant('cytoscapeSettings', cytoscapeSettings) diff --git a/client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/index.js b/client/client/app/shared/components/ngbCytoscapeToolbarPanel/index.js similarity index 100% rename from client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/index.js rename to client/client/app/shared/components/ngbCytoscapeToolbarPanel/index.js diff --git a/client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.component.js b/client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.component.js similarity index 100% rename from client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.component.js rename to client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.component.js diff --git a/client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.controller.js b/client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.controller.js similarity index 100% rename from client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.controller.js rename to client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.controller.js diff --git a/client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.scss b/client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.scss similarity index 100% rename from client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.scss rename to client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.scss diff --git a/client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html b/client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html similarity index 59% rename from client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html rename to client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html index 694ae05c0..81b62fa9f 100644 --- a/client/client/app/components/ngbStrainLineage/ngbCytoscape/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html +++ b/client/client/app/shared/components/ngbCytoscapeToolbarPanel/ngbCytoscapeToolbarPanel.tpl.html @@ -1,10 +1,15 @@
- + - - diff --git a/client/client/dataServices/genome/genome-data-service.js b/client/client/dataServices/genome/genome-data-service.js index 3f3f7ced4..50675912c 100644 --- a/client/client/dataServices/genome/genome-data-service.js +++ b/client/client/dataServices/genome/genome-data-service.js @@ -193,9 +193,6 @@ export class GenomeDataService extends DataService { }); } - _orthoPara = []; - _orthoParaResult = []; - getHomologeneLoad(filter) { return new Promise((resolve, reject) => { this.post('homologene/search', filter) @@ -210,6 +207,22 @@ export class GenomeDataService extends DataService { }); } + _internalPathways = { + items: [ + { + name: 'activated_stat1alpha_induction_of_the_irf1_gene', + description: 'desc1', + xml: 'activated_stat1alpha_induction_of_the_irf1_gene' + }, + { + name: 'Glycolysis', + description: 'desc2', + xml: 'Glycolysis' + }, + ], + totalCount: 2 + }; + getOrthoParaLoad(filter) { return new Promise((resolve, reject) => { this.post('homolog/search', filter) @@ -224,6 +237,21 @@ export class GenomeDataService extends DataService { }); } + getInternalPathwaysLoad(filter) { + return this._internalPathways; + return new Promise((resolve, reject) => { + this.post('homolog/search', filter) + .then((data) => { + if (data) { + resolve(data); + } else { + reject(new Error('No orthologs or paralogs received')); + } + }) + .catch(reject); + }); + } + getAllLineageTrees() { return new Promise((resolve, reject) => { this.get('lineage/trees/all') diff --git a/client/package.json b/client/package.json index 0f3c5dc75..201c5b919 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,8 @@ "cytoscape": "^3.20.0", "cytoscape-dagre": "^2.3.2", "cytoscape-dom-node": "^1.1.0", + "cytoscape-graphml": "^1.0.6", + "cytoscape-sbgn-stylesheet": "^4.0.2", "deep-extend": "^0.4.1", "golden-layout": "1.5.5", "jquery": "3.3.1", @@ -44,6 +46,7 @@ "moment": "^2.23.0", "pixi.js-legacy": "6.1.3", "rx": "4.1.0", + "sbgnml-to-cytoscape": "^4.0.4", "showdown": "^1.9.1", "tether": "1.3.2" }, @@ -75,14 +78,14 @@ "lodash": "^4.17.21", "minimist": "1.2.0", "ng-annotate-loader": "0.1.0", - "node-sass": "^4.12.0", + "node-sass": "4.14.0", "null-loader": "^0.1.1", "postcss-cssnext": "2.5.2", "postcss-import": "8.1.2", "postcss-loader": "^0.7.0", "postcss-normalize": "0.2.0", "pug-loader": "^1.0.2", - "raw-loader": "0.5.1", + "raw-loader": "^0.5.1", "sass-lint": "1.13.1", "sass-loader": "7.3.1", "style-loader": "^0.13.0", diff --git a/client/webpack.config.babel/webpack.loaders.js b/client/webpack.config.babel/webpack.loaders.js index 2ce5f50dc..5d4354369 100644 --- a/client/webpack.config.babel/webpack.loaders.js +++ b/client/webpack.config.babel/webpack.loaders.js @@ -1,4 +1,5 @@ import {extractTextPlugin} from './webpack.plugins'; + const DEV = global.buildOptions.dev; const wrapStyleLoader = loader => DEV @@ -59,6 +60,17 @@ const imagesLoader = { loader: 'url-loader?name=[name].[ext]' }; +const xmlLoader = { + // ASSET LOADER + // Reference: https://github.com/webpack/file-loader + // Copy png, jpg, jpeg, gif, svg, woff, woff2, ttf, eot files to output + // Rename the file using the asset hash + // Pass along the updated reference to your code + // You can add here any file extension you want to get copied to your output + test: /\.xml$/i, + loader: 'raw-loader', +}; + const sassLoader = { // Sass Loader test: /\.scss$/, @@ -82,6 +94,7 @@ export default [ commonStyleLoader, HTMLLoader, imagesLoader, + xmlLoader, sassLoader, {test: /\.woff$/, loader: 'url?limit=65000&mimetype=application/font-woff&name=[name].[ext]'}, {test: /\.woff2$/, loader: 'url?limit=65000&mimetype=application/font-woff2&name=[name].[ext]'}, From 403c6c46d1f17b9ad10120cbe73137e29623bfa2 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Fri, 28 Jan 2022 18:35:48 +0300 Subject: [PATCH 02/14] Metabolic pathways visualisation (#731): internal pathways api integration + layout fixes --- .../ngbCytoscapePathway.controller.js | 72 ++++------ .../ngbCytoscapePathway.settings.js | 38 +----- .../ngbInternalPathwaysResult.controller.js | 7 +- .../ngbInternalPathwaysResult.scss | 8 +- .../ngbInternalPathwaysResult.service.js | 17 +-- .../ngbInternalPathwaysResult.tpl.html | 6 +- .../ngbInternalPathwaysTable.controller.js | 14 +- .../ngbInternalPathwaysTable.service.js | 127 +++--------------- .../ngbInternalPathwaysTable_header.tpl.html | 60 +++++++++ .../ngbPathways/ngbPathways.service.js | 2 +- .../ngbPathways/ngbPathwaysPanel.html | 26 ++-- .../ngbPathways/ngbPathwaysPanel.scss | 28 ++++ client/client/dataServices/data-service.js | 19 ++- .../genome/genome-data-service.js | 20 ++- 14 files changed, 203 insertions(+), 241 deletions(-) create mode 100644 client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable_header.tpl.html diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index f842bd961..3fd89dd93 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -6,10 +6,6 @@ const sbgnStylesheet = require('cytoscape-sbgn-stylesheet'); const $ = require('jquery'); const SCALE = 0.3; -const elementOptionsType = { - NODE: 'nodes', - EDGE: 'edges' -}; export default class ngbCytoscapePathwayController { constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapeSettings) { @@ -71,27 +67,25 @@ export default class ngbCytoscapePathwayController { } this.$timeout(() => { const sbgnStyle = sbgnStylesheet(Cytoscape); - // Object.keys(sbgnStyle).forEach(key => { - // if(sbgnStyle[key].selector === 'edge') { - // Object.keys(sbgnStyle[key].properties).forEach(propKey => { - // if(sbgnStyle[key].properties[propKey].name === 'curve-style') { - // sbgnStyle[key].properties[propKey].value = 'taxi'; - // } - // }); - // } - // }); - // sbgnStyle.edge['curve-style'] = 'taxi'; - // const savedState = JSON.parse(localStorage.getItem(this.storageName) || '{}'); - // const savedLayout = savedState.layout ? savedState.layout[this.elements.id] : undefined; - // let elements, layoutSettings; + const savedState = JSON.parse(localStorage.getItem(this.storageName) || '{}'); + const savedLayout = savedState.layout ? savedState.layout[this.elements.id] : undefined; + let elements; + if (savedLayout) { + elements = { + nodes: this.getPlainNodes(savedLayout.nodes), + edges: savedLayout.edges + }; + } else { + elements = { + nodes: this.positionedNodes(this.elements.nodes), + edges: this.elements.edges + }; + } this.viewer = Cytoscape({ container: this.cytoscapeContainer, style: sbgnStyle, layout: {name: 'preset'}, - elements: { - nodes: this.positionedNodes(this.elements.nodes), - edges: this.elements.edges, - }, + elements: elements, }); const layout = this.viewer.layout(this.settings.loadedLayout); layout.on('layoutready', () => { @@ -120,7 +114,7 @@ export default class ngbCytoscapePathwayController { layout.run(); const viewerContext = this; this.actionsManager = { - ZOOM_STEP: 0.25, + ZOOM_STEP: 0.125, duration: 250, zoom: () => viewerContext.viewer.zoom(), zoomIn() { @@ -137,6 +131,16 @@ export default class ngbCytoscapePathwayController { this.canZoomIn = zoom < viewerContext.viewer.maxZoom(); this.canZoomOut = zoom > viewerContext.viewer.minZoom(); }, + restoreDefault: () => { + this.viewer.batch(() => { + this.viewer.remove(this.viewer.nodes()); + this.viewer.remove(this.viewer.edges()); + this.viewer.add(this.positionedNodes(this.elements.nodes)); + this.viewer.add(this.elements.edges); + }); + // viewerContext.viewer.layout(this.settings.defaultLayout).run(); + viewerContext.saveLayout(); + }, canZoomIn: true, canZoomOut: true, ready: true @@ -195,32 +199,12 @@ export default class ngbCytoscapePathwayController { }, []); } - applyOptions(elements, type) { - if (!this.elementsOptions) { - return; - } - switch (type) { - case elementOptionsType.NODE: { - return elements.reduce((r, cv) => { - if (this.elementsOptions.nodes[cv.data.id]) { - cv.data = { - ...cv.data, - ...this.elementsOptions.nodes[cv.data.id] - }; - } - r.push(cv); - return r; - }, []); - } - } - } - positionedNodes(nodes) { nodes.forEach(node => { if (node.data.bbox) { node.position = { - x: node.data.bbox.x/SCALE, - y: node.data.bbox.y/SCALE + x: node.data.bbox.x / SCALE, + y: node.data.bbox.y / SCALE }; } }); diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js index 4347a89de..2cc400569 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js @@ -1,42 +1,6 @@ export default { viewer: {}, - style: { - node: { - 'content': 'data(id)', - 'width': 175, - 'height': 60, - 'text-opacity': 1, - 'text-valign': 'center', - 'text-halign': 'center', - 'shape': 'rectangle', - 'label': 'data(id)', - 'background-opacity': 0, - 'opacity': 0, - 'color': '#000', - 'border-width': 1 - }, - edge: { - 'curve-style': 'bezier', - 'width': 1, - 'line-color': '#37474F', - 'target-arrow-color': '#37474F', - 'target-arrow-shape': 'triangle', - 'overlay-color': '#4285F4', - 'overlay-padding': '4px', - 'underlay-color': '#4285F4', - 'underlay-padding': '3px', - 'underlay-opacity': '0' - }, - edgeLabel: { - 'text-rotation': 'none', - 'content': 'data(label)', - 'font-size': '12px', - 'font-weight': 'bold', - 'text-background-color': '#fff', - 'text-background-opacity': 1, - 'color': '#2c4f9e' - } - }, + style: {}, 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 ab1af1b39..d9eaecb4a 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js @@ -47,16 +47,15 @@ export default class ngbInternalPathwaysResultController extends baseController } async initialize() { - if (!this.ngbPathwaysService.currentInternalPathwaysId) { + if (!this.ngbPathwaysService.currentInternalPathway) { return; } - const {data, error} = await this.ngbInternalPathwaysResultService.getPathwayTreeById(this.ngbPathwaysService.currentInternalPathwaysId); + const {data, error} = await this.ngbInternalPathwaysResultService.getPathwayTree(this.ngbPathwaysService.currentInternalPathway); if (error) { this.treeError = error; } else { this.treeError = false; - this.selectedTree = data.tree; - this.selectedTreeName = data.name; + this.selectedTree = data; } this.loading = false; this.$timeout(() => this.$scope.$apply()); diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss index 149b7c7ea..bf544dfca 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -1,8 +1,6 @@ -.ngb-cytoscape-container { - width: 100%; - height: 100%; - min-height: 500px; +.ngb-pathway-cytoscape-container { flex: 1; + position: relative; } .internal-pathway-container-error { @@ -11,7 +9,7 @@ } .internal-pathway-container { - margin: 2px 10px; + margin: 2px 0; height: calc(100% - 4px); display: flex; flex-direction: column; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js index 2edd80759..5211cc21c 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.service.js @@ -1,7 +1,4 @@ export default class ngbInternalPathwaysResultService { - - currentReferenceId = null; - constructor(genomeDataService, dispatcher) { this.dispatcher = dispatcher; this.genomeDataService = genomeDataService; @@ -11,24 +8,22 @@ export default class ngbInternalPathwaysResultService { return new ngbInternalPathwaysResultService(genomeDataService, dispatcher); } - async getPathwayTreeById(id) { - if(!id) { + async getPathwayTree(treeConfig) { + if(!treeConfig.id) { return { data: null, error: false }; } - const xml = require(`./xml/${id}.xml`); + const xml = await this.genomeDataService.loadPathwayFileById(treeConfig.id); try { const convert = require('sbgnml-to-cytoscape'); const data = convert(xml); + data.id = treeConfig.id; + data.name = treeConfig.name; if (data) { - data.id = id; return { - data: { - name: id, - tree: data - }, + data: data, error: false }; } else { diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index e4374fa4b..170a8ea8b 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -4,7 +4,7 @@ -
+
- {{$ctrl.selectedTreeName}} + {{$ctrl.selectedTree.name}}
-
+
{{$ctrl.treeError}}
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js index 419d052cb..683909ce9 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.controller.js @@ -7,7 +7,6 @@ const RESIZE_DELAY = 300; export default class ngbInternalPathwaysTableController extends baseController { dispatcher; isProgressShown = true; - isEmptyResult = false; errorMessageList = []; debounce = (new Debounce()).debounce; gridOptions = { @@ -39,7 +38,7 @@ export default class ngbInternalPathwaysTableController extends baseController { }; events = { 'pathways:internalPathways:page:change': this.loadData.bind(this), - 'pathways:internalPathways:search': this.loadData.bind(this), + 'pathways:internalPathways:search': this.initialize.bind(this), 'read:show:pathways': this.loadData.bind(this) }; @@ -87,10 +86,10 @@ export default class ngbInternalPathwaysTableController extends baseController { } }); await this.loadData(); + this.isProgressShown = false; } async loadData() { - this.isProgressShown = true; try { await this.ngbInternalPathwaysTableService.searchInternalPathways(this.ngbPathwaysService.currentSearch); const dataLength = this.ngbInternalPathwaysTableService.internalPathways.length; @@ -106,9 +105,7 @@ export default class ngbInternalPathwaysTableController extends baseController { } else { this.isEmptyResults = true; } - this.isProgressShown = false; } catch (errorObj) { - this.isProgressShown = false; this.onError(errorObj.message); } this.$timeout(() => this.$scope.$apply()); @@ -121,7 +118,10 @@ export default class ngbInternalPathwaysTableController extends baseController { rowClick(row, event) { const entity = row.entity; if (entity) { - this.ngbPathwaysService.currentInternalPathwaysId = row.entity.xml; + this.ngbPathwaysService.currentInternalPathway = { + id: row.entity.id, + name: row.entity.name + }; this.changeState({state: 'INTERNAL_PATHWAYS_RESULT'}); } else { event.stopImmediatePropagation(); @@ -136,7 +136,7 @@ export default class ngbInternalPathwaysTableController extends baseController { const {columns} = this.gridApi.saveState.save(); const fieldTitleMap = ( o => Object.keys(o).reduce( - (r, k) => Object.assign(r, { [o[k]]: k }), {} + (r, k) => Object.assign(r, {[o[k]]: k}), {} ) )(this.ngbInternalPathwaysTableService.columnTitleMap); const mapNameToField = function ({name}) { diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js index cd7b91eaa..2bb558d67 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js @@ -1,5 +1,4 @@ import ClientPaginationService from '../../../shared/services/clientPaginationService'; -import {calculateColor} from '../../../shared/utils/calculateColor'; const DEFAULT_INTERNAL_PATHWAYS_COLUMNS = [ 'name', 'description' @@ -13,12 +12,9 @@ const INTERNAL_PATHWAYS_COLUMN_TITLES = { description: 'Description' }; const FIRST_PAGE = 1; -const PAGE_SIZE = 15; +const PAGE_SIZE = 11; export default class ngbInternalPathwaysTableService extends ClientPaginationService { - - _internalPathwaysResult; - constructor(dispatcher, genomeDataService) { super(dispatcher, FIRST_PAGE, PAGE_SIZE, 'pathways:internalPathways:page:change'); this.dispatcher = dispatcher; @@ -60,30 +56,18 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer return new ngbInternalPathwaysTableService(dispatcher, genomeDataService); } - getInternalPathwaysResultById(id) { - return this._internalPathwaysResult[id]; - } - - getInternalPathwaysById(id) { - return this.internalPathways.filter(h => h.groupId === id)[0] || {}; - } - async searchInternalPathways(currentSearch) { - const result = await this.loadInternalPathways(currentSearch); - this._internalPathways = result.internalPathways; - this._internalPathwaysResult = result.internalPathwaysResult; + this._internalPathways = await this.loadInternalPathways(currentSearch); this.dispatcher.emitSimpleEvent('internalPathways:result:change'); } async loadInternalPathways(currentSearch) { - const emptyResult = { - internalPathways: [], - internalPathwaysResult: {} - }; const filter = { - query: currentSearch, - page: this.currentPage, - pageSize: this.pageSize + pagingInfo: { + pageSize: this.pageSize, + pageNum: this.currentPage + }, + sortInfos: this.orderBy }; const data = await this.genomeDataService.getInternalPathwaysLoad(filter); if (data.error) { @@ -91,58 +75,22 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer this.currentPage = FIRST_PAGE; this._firstPage = FIRST_PAGE; this._pageError = data.message; - return emptyResult; + return []; } else { this._pageError = null; } this.totalPages = Math.ceil(data.totalCount / this.pageSize); if (data && data.items) { - return { - internalPathways: this.getInternalPathwaysSearch(data.items), - internalPathwaysResult: this.getInternalPathwaysResult(data.items) - }; + return data.items.map(this._formatServerToClient); } else { - return emptyResult; + return []; } } - getInternalPathwaysSearch(data) { - const result = []; - data.forEach(value => result.push(this._formatServerToClient(value))); - return result; - } - - getInternalPathwaysResult(data) { - return data; - let maxHomologLength = 0; - const result = {}; - if (data) { - data.forEach(internalPathways => { - maxHomologLength = 0; - result[internalPathways.groupId] = []; - internalPathways.genes.forEach((gene, key) => { - result[internalPathways.groupId][key] = this._formatResultToClient(gene); - if (maxHomologLength < result[internalPathways.groupId][key].aa) { - maxHomologLength = result[internalPathways.groupId][key].aa; - } - }); - result[internalPathways.groupId].forEach((value, key) => { - result[internalPathways.groupId][key].domainsObj = { - domains: value.domains.map(d => ({...d, color: calculateColor(d.name)})), - homologLength: value.aa, - maxHomologLength: maxHomologLength, - accession_id: value.accession_id - }; - delete result[internalPathways.groupId][key].domains; - }); - }); - } - return result; - } - getInternalPathwaysGridColumns() { const result = []; const columnsList = this.internalPathwaysColumns; + const headerCells = require('./ngbInternalPathwaysTable_header.tpl.html'); for (let i = 0; i < columnsList.length; i++) { let sortDirection = 0; let sortingPriority = 0; @@ -162,8 +110,9 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer cellTemplate: ``, enableHiding: false, - enableColumnMenu: false, + enableSorting: true, field: 'name', + headerCellTemplate: headerCells, name: this.columnTitleMap[column] }; break; @@ -171,9 +120,9 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer default: { columnSettings = { enableHiding: false, - enableColumnMenu: false, field: column, minWidth: 40, + headerCellTemplate: headerCells, name: this.columnTitleMap[column], width: '*' }; @@ -194,52 +143,10 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer } _formatServerToClient(internalPathways) { - return internalPathways; - const gene = new Set(); - const proteinFrequency = {}; - internalPathways.genes.forEach(g => { - gene.add(g.symbol); - if (proteinFrequency.hasOwnProperty(g.title)) { - proteinFrequency[g.title] += 1; - } else { - proteinFrequency[g.title] = 1; - } - }); - - const sortableProteinFrequency = []; - for (const protein in proteinFrequency) { - if (proteinFrequency.hasOwnProperty(protein)) { - sortableProteinFrequency.push([protein, proteinFrequency[protein]]); - } - } - - sortableProteinFrequency.sort((b, a) => a[1] - b[1]); - - return { - groupId: internalPathways.groupId, - gene: [...gene].sort().join(', '), - protein: sortableProteinFrequency[0] ? sortableProteinFrequency[0][0] : '', - info: internalPathways.caption - }; - } - - _formatResultToClient(result) { return { - geneId: result.geneId, - name: result.symbol, - species: result.speciesScientificName, - accession_id: result.protAcc, - protGi: result.protGi, - aa: result.protLen, - taxId: result.taxId, - protein: result.title, - domains: (result.domains || []).map(d => ({ - id: d.pssmId, - start: d.begin, - end: d.end, - name: d.cddName - })) + id: internalPathways.pathwayId, + name: internalPathways.prettyName || internalPathways.name, + description: internalPathways.pathwayDesc }; } - } diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable_header.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable_header.tpl.html new file mode 100644 index 000000000..7f7c08195 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable_header.tpl.html @@ -0,0 +1,60 @@ +
+
+
+ + + {{ col.displayName }} + + + + + + + + {{col.sort.priority + 1}} + + + +
+
+
diff --git a/client/client/app/components/ngbPathways/ngbPathways.service.js b/client/client/app/components/ngbPathways/ngbPathways.service.js index 1004ccf1f..5572861ca 100644 --- a/client/client/app/components/ngbPathways/ngbPathways.service.js +++ b/client/client/app/components/ngbPathways/ngbPathways.service.js @@ -6,7 +6,7 @@ const PATHWAYS_STATES = { export default class ngbPathwaysService { pathwaysServiceMap = {}; - currentInternalPathwaysId; + currentInternalPathway; constructor(dispatcher, projectContext, ngbInternalPathwaysTableService, ngbInternalPathwaysResultService diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.html b/client/client/app/components/ngbPathways/ngbPathwaysPanel.html index c57b444e4..665618f66 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.html +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.html @@ -1,17 +1,19 @@
- - - +
+ + + + Search - +
@@ -39,12 +41,12 @@ ng-if="$ctrl.currentPathwaysState === $ctrl.pathwaysStates.KEGG"> + class="blast-search-flex-column" + flex + ng-if="$ctrl.currentPathwaysState === $ctrl.pathwaysStates.INTERNAL_PATHWAYS"> + class="blast-search-flex-column" + flex + ng-if="$ctrl.currentPathwaysState === $ctrl.pathwaysStates.INTERNAL_PATHWAYS_RESULT">
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss index 9e3695cef..860bddb8d 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss @@ -2,11 +2,13 @@ color: rgb(51, 103, 214); font-weight: bold; cursor: pointer; + a { color: inherit; text-decoration: none; } } + .pathways-search-result { padding: 5px 2px; @@ -17,6 +19,32 @@ font-size: 18px; } } + .pathways-no-feature { font-weight: bold; } + +.pathways-search-container { + width: 100%; + display: flex; + margin-top: 10px; + + .pathways-search-input { + font-size: 14px; + margin: 5px 0 10px 6px; + + .pathways-search-input-label { + padding-bottom: 2px; + } + } + .pathways-search-button { + margin: 6px 2px 6px 10px; + line-height: 32px; + min-height: 32px; + height: 32px; + } +} + +.pathways-search-input .md-errors-spacer { + min-height: 0; +} diff --git a/client/client/dataServices/data-service.js b/client/client/dataServices/data-service.js index fc6680b36..67dae878c 100644 --- a/client/client/dataServices/data-service.js +++ b/client/client/dataServices/data-service.js @@ -1,9 +1,6 @@ -import { - SessionExpirationBehavior, - SessionExpirationBehaviorStorageKey -} from './utils/session-expiration-behavior'; import BluebirdPromise from 'bluebird'; import ngbConstants from '../constants'; +import {SessionExpirationBehavior, SessionExpirationBehaviorStorageKey} from './utils/session-expiration-behavior'; const AUTH_ERROR_CODE = 401; const ERROR_CODE_RANGE_START = 400; @@ -101,6 +98,20 @@ export class DataService { }); } + getRawFile(method, url, data, ...rest) { + return $http(method, this._serverUrl + url, data, ...rest) + .then((xhr) => { + if (xhr.status === AUTH_ERROR_CODE) { + this.handleAuthenticationError(); + return Promise.reject(xhr.response); + } + if (xhr.status >= ERROR_CODE_RANGE_START) { + return Promise.reject(xhr.response); + } + return xhr.responseText; + }); + } + getFullUrl(url) { return this._serverUrl + url; } diff --git a/client/client/dataServices/genome/genome-data-service.js b/client/client/dataServices/genome/genome-data-service.js index 50675912c..198101ddc 100644 --- a/client/client/dataServices/genome/genome-data-service.js +++ b/client/client/dataServices/genome/genome-data-service.js @@ -238,14 +238,28 @@ export class GenomeDataService extends DataService { } getInternalPathwaysLoad(filter) { - return this._internalPathways; + // return this._internalPathways; return new Promise((resolve, reject) => { - this.post('homolog/search', filter) + this.post('pathways', filter) .then((data) => { if (data) { resolve(data); } else { - reject(new Error('No orthologs or paralogs received')); + reject(new Error('No pathways received')); + } + }) + .catch(reject); + }); + } + + loadPathwayFileById(id) { + return new Promise((resolve, reject) => { + this.getRawFile('get', `pathway/content/${id}`, null, {customResponseType: 'text'}) + .then((data) => { + if (data) { + resolve(data); + } else { + reject(new Error('No pathway info received')); } }) .catch(reject); From 5c9f93d9892855c84362a590a97467ae1904f573 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Sun, 30 Jan 2022 13:07:00 +0300 Subject: [PATCH 03/14] Metabolic pathways visualisation (#731): context menu navigation --- .../ngbGenesTableContextMenu.controller.js | 16 +++++++++++++ .../ngbGenesTableContextMenu.template.html | 10 ++++++++ .../ngbPathways/ngbPathways.service.js | 2 +- .../ngbPathwaysPanel.controller.js | 3 ++- .../ngbTracksView/ngbTrack/ngbTrack.events.js | 24 +++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.controller.js b/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.controller.js index 73912dacd..3872fe001 100644 --- a/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.controller.js +++ b/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.controller.js @@ -119,4 +119,20 @@ export default class NgbGenesTableContextMenuController extends BaseController { this.dispatcher.emitSimpleEvent('read:show:homologs', data); event.stopImmediatePropagation(); } + + pathwaysSearch() { + this.close(); + const layoutChange = this.appLayout.Panels.pathways; + layoutChange.displayed = true; + this.dispatcher.emitSimpleEvent('layout:item:change', {layoutChange}); + const readInfo = { + search: this.entity[`${this.ngbGenesTableService.defaultPrefix}featureName`] + }; + const data = { + ...readInfo, + source: 'gene' + }; + this.dispatcher.emitSimpleEvent('read:show:pathways', data); + event.stopImmediatePropagation(); + } } diff --git a/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.template.html b/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.template.html index 80853728e..494e9475e 100644 --- a/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.template.html +++ b/client/client/app/components/ngbGenesTablePanel/ngbGenesTable/ngbGenesTableContextMenu/ngbGenesTableContextMenu.template.html @@ -30,5 +30,15 @@ + + + Show pathways + + + Feature type is not Gene or Name is missing + + +
diff --git a/client/client/app/components/ngbPathways/ngbPathways.service.js b/client/client/app/components/ngbPathways/ngbPathways.service.js index 5572861ca..4c485c3de 100644 --- a/client/client/app/components/ngbPathways/ngbPathways.service.js +++ b/client/client/app/components/ngbPathways/ngbPathways.service.js @@ -47,7 +47,7 @@ export default class ngbPathwaysService { initEvents() { this.dispatcher.on('read:show:pathways', data => { - this.currentSearch = data; + this.currentSearch = data ? data.search : null; }); } } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js index e01bb83e5..e4db9b594 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js @@ -8,7 +8,8 @@ export default class ngbPathwaysPanelController extends baseController { searchRequest; events = { - 'read:show:pathways': () => { + 'read:show:pathways': data => { + this.searchRequest = data ? data.search : null; this.changeState('INTERNAL_PATHWAYS'); } }; diff --git a/client/client/app/components/ngbTracksView/ngbTrack/ngbTrack.events.js b/client/client/app/components/ngbTracksView/ngbTrack/ngbTrack.events.js index 81a5b11a6..b48a2f23d 100644 --- a/client/client/app/components/ngbTracksView/ngbTrack/ngbTrack.events.js +++ b/client/client/app/components/ngbTracksView/ngbTrack/ngbTrack.events.js @@ -217,6 +217,30 @@ export default class ngbTrackEvents { : undefined, }); } + if (data.feature.feature + && data.feature.feature.toLowerCase() === 'gene' + && data.feature.name) { + const layoutChange = this.appLayout.Panels.pathways; + layoutChange.displayed = true; + menuData.push({ + events: [ + { + data: {layoutChange}, + name: 'layout:item:change' + }, + { + data: { + search: data.feature.name + }, + name: 'read:show:pathways' + }], + title: 'Show pathways', + disabled: (data.feature.feature || '').toLowerCase() !== 'gene' || !data.feature.name, + warning: (data.feature.feature || '').toLowerCase() !== 'gene' || !data.feature.name + ? 'Feature type is not Gene or Name is missing' + : undefined, + }); + } if (menuData.length > 0) { const childScope = this.$scope.$new(false); childScope.menuData = menuData; From 572806964381db2ede7b9ef108bc71215190bc37 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Tue, 1 Feb 2022 17:31:08 +0300 Subject: [PATCH 04/14] 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 @@ {{$ctrl.selectedTree.name}}
+
+
+
+
+
+ + + + + + +
+
+
+
+
@@ -26,6 +45,7 @@ ng-if="!$ctrl.treeError" storage-name="{{$ctrl.ngbInternalPathwaysResultService.localStorageKey}}" tag="ngb-internal-pathway-node" + search-params="$ctrl.treeSearchParams" >
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) { From 7ec54f097780153176a8c18f97e3c3a6767ff2e6 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Wed, 2 Feb 2022 13:24:05 +0300 Subject: [PATCH 05/14] Metabolic pathways visualisation (#731): xml loader remove --- client/package.json | 2 +- client/webpack.config.babel/webpack.loaders.js | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/client/package.json b/client/package.json index 201c5b919..8486847bc 100644 --- a/client/package.json +++ b/client/package.json @@ -85,7 +85,7 @@ "postcss-loader": "^0.7.0", "postcss-normalize": "0.2.0", "pug-loader": "^1.0.2", - "raw-loader": "^0.5.1", + "raw-loader": "0.5.1", "sass-lint": "1.13.1", "sass-loader": "7.3.1", "style-loader": "^0.13.0", diff --git a/client/webpack.config.babel/webpack.loaders.js b/client/webpack.config.babel/webpack.loaders.js index 5d4354369..30da7b3bc 100644 --- a/client/webpack.config.babel/webpack.loaders.js +++ b/client/webpack.config.babel/webpack.loaders.js @@ -60,17 +60,6 @@ const imagesLoader = { loader: 'url-loader?name=[name].[ext]' }; -const xmlLoader = { - // ASSET LOADER - // Reference: https://github.com/webpack/file-loader - // Copy png, jpg, jpeg, gif, svg, woff, woff2, ttf, eot files to output - // Rename the file using the asset hash - // Pass along the updated reference to your code - // You can add here any file extension you want to get copied to your output - test: /\.xml$/i, - loader: 'raw-loader', -}; - const sassLoader = { // Sass Loader test: /\.scss$/, @@ -94,7 +83,6 @@ export default [ commonStyleLoader, HTMLLoader, imagesLoader, - xmlLoader, sassLoader, {test: /\.woff$/, loader: 'url?limit=65000&mimetype=application/font-woff&name=[name].[ext]'}, {test: /\.woff2$/, loader: 'url?limit=65000&mimetype=application/font-woff2&name=[name].[ext]'}, From 546c5daad6fffc81e3ab23a5aa4db17795c66194 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Mon, 7 Feb 2022 11:58:39 +0300 Subject: [PATCH 06/14] Metabolic pathways visualisation (#731): manual annotations --- .../ngbCytoscapePathway.controller.js | 132 +++++++++++++----- .../ngbCytoscapePathway.settings.js | 3 +- .../ngbInternalPathwaysResult.controller.js | 55 +++++++- .../ngbInternalPathwaysResult.scss | 31 +++- .../ngbInternalPathwaysResult.tpl.html | 27 ++++ .../ngbPathways/ngbPathways.service.js | 65 +++++++++ .../ngbPathwaysAnnotationAddDlg.controller.js | 80 +++++++++++ .../ngbPathwaysAnnotationAddDlg.tpl.html | 51 +++++++ .../ngbPathways/ngbPathwaysPanel.scss | 12 ++ 9 files changed, 413 insertions(+), 43 deletions(-) create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index 38d2e55f6..865b4c0e0 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -9,6 +9,24 @@ const SCALE = 0.3; const searchedColor = '#00cc00'; let defaultNodeStyle = {}; +function 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 = deepSearch(obj[key], term); + } else { + result = obj[key].toString().toLocaleLowerCase() + .includes(term.toLocaleLowerCase()); + } + if (result) { + return true; + } + } + return false; +} + + export default class ngbCytoscapePathwayController { constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapeSettings) { this.$scope = $scope; @@ -62,23 +80,12 @@ export default class ngbCytoscapePathwayController { if (!!changes.searchParams && !!changes.searchParams.previousValue && !!changes.searchParams.currentValue) { - if (changes.searchParams.currentValue.search + if (changes.searchParams.currentValue.search !== null && 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'] - }); - } - ); + this.searchTree(changes.searchParams.currentValue.search); + } + if (changes.searchParams.currentValue.annotations && changes.searchParams.currentValue.annotations.length) { + this.annotateTree(changes.searchParams.currentValue.annotations); } } } @@ -120,6 +127,9 @@ export default class ngbCytoscapePathwayController { this.$compile(this.cytoscapeContainer)(this.$scope); this.viewer.on('dragfree', this.saveLayout.bind(this)); this.resizeCytoscape(); + if(this.searchParams.annotations && this.searchParams.annotations.length) { + this.annotateTree(this.searchParams.annotations); + } }); this.viewer.edges().on('click', e => { const edgeData = e.target.data(); @@ -218,6 +228,8 @@ export default class ngbCytoscapePathwayController { getPlainNodes(nodes) { return nodes.reduce((r, cv) => { delete cv.data.dom; + cv.data.isFound = false; + cv.data.isAnnotated = false; r.push({ data: cv.data, position: cv.position, @@ -239,38 +251,82 @@ export default class ngbCytoscapePathwayController { return nodes; } - searchNode(term, onSatisfy, onDeny) { + + searchTree(term) { 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); - } + this.viewer.nodes().forEach(node => { + this.searchNode(node, term); + }); + } + + searchNode(node, term) { + if (term === '' || !deepSearch(node.data(), term)) { + node.style({ + 'color': defaultNodeStyle.color, + 'border-color': defaultNodeStyle['border-color'], + 'font-weight': defaultNodeStyle['font-weight'] + }); + node.data('isFound', false); + if (node.data('isAnnotated') && this.searchParams.annotations) { + this.annotateNode(node, this.searchParams.annotations); } + } else { + node.style({ + 'color': searchedColor, + 'border-color': searchedColor, + 'background-color': defaultNodeStyle['background-color'], + 'font-weight': 'bold' + }); + node.data('isFound', true); + } + } + + annotateTree(annotationList) { + if (!this.viewer) { + return; + } + // TODO: figure out why dfs() ignores half of a tree (roots?) + // const roots = this.viewer.nodes().roots(); + // this.viewer.nodes().dfs({ + // root: roots, + // visit: node => { + this.viewer.nodes().forEach(node => { + this.annotateNode(node, annotationList); }); } - 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()); + annotateNode(node, annotationList) { + let style = {}; + if (node.data('isAnnotated')) { + node.data('isAnnotated', false); + if (!node.data('isFound')) { + style = { + 'background-color': defaultNodeStyle['background-color'], + color: defaultNodeStyle.color, + 'border-color': defaultNodeStyle['border-color'] + }; + node.style(style); } - if (result) { - return true; + } + for (const annotation of annotationList) { + for (const termsList of annotation.value) { + if (deepSearch(node.data(), termsList.term) && !node.data('isAnnotated')) { + node.data('isAnnotated', true); + if (!node.data('isFound')) { + style = { + 'background-color': termsList.backgroundColor + }; + if (termsList.foregroundColor) { + style.color = termsList.foregroundColor; + style['border-color'] = termsList.foregroundColor; + } + node.style(style); + } + } } } - return false; } getNodeStyle(style) { diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js index 4846ebd6d..8a8341e79 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js @@ -7,7 +7,8 @@ export default { 'text-halign': 'center', 'shape': 'rectangle', 'color': '#000', - 'border-width': 1 + 'border-width': 1, + 'font-weight': 'normal' } }, defaultLayout: { diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js index 533a3747f..8c32433fc 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js @@ -1,4 +1,6 @@ +import angular from 'angular'; import baseController from '../../../shared/baseController'; +import ngbPathwaysAnnotationAddDlgController from '../ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller'; export default class ngbInternalPathwaysResultController extends baseController { selectedTree = null; @@ -13,6 +15,7 @@ export default class ngbInternalPathwaysResultController extends baseController 'layout:active:panel:change': this.activePanelChanged.bind(this), 'reference:change': this.initialize.bind(this), 'reference:show:pathway': this.initialize.bind(this), + 'pathways:internalPathways:annotations:change': this.refreshAnnotationList.bind(this) }; constructor( @@ -23,7 +26,8 @@ export default class ngbInternalPathwaysResultController extends baseController ngbPathwaysService, appLayout, projectContext, - localDataService + localDataService, + $mdDialog ) { super(); @@ -37,7 +41,8 @@ export default class ngbInternalPathwaysResultController extends baseController ngbPathwaysService, appLayout, projectContext, - localDataService + localDataService, + $mdDialog } ); @@ -60,16 +65,62 @@ export default class ngbInternalPathwaysResultController extends baseController this.treeError = false; this.selectedTree = data; } + this.refreshAnnotationList(); this.loading = false; this.$timeout(() => this.$scope.$apply()); } searchInTree() { this.treeSearchParams = { + ...this.treeSearchParams, search: this.treeSearch }; } + applyAnnotations() { + this.treeSearchParams = { + ...this.treeSearchParams, + annotations: this.annotationList.filter(a => a.isActive) + }; + this.ngbPathwaysService.saveAnnotationList(this.annotationList); + } + + // FIXME: separate annotation component + addAnnotation() { + this.$mdDialog.show({ + clickOutsideToClose: true, + controller: ngbPathwaysAnnotationAddDlgController, + controllerAs: '$ctrl', + parent: angular.element(document.body), + template: require('../ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html'), + locals: { + annotation: null + } + }); + } + + editAnnotation(id) { + this.$mdDialog.show({ + clickOutsideToClose: true, + controller: ngbPathwaysAnnotationAddDlgController, + controllerAs: '$ctrl', + parent: angular.element(document.body), + template: require('../ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html'), + locals: { + annotation: this.ngbPathwaysService.getAnnotationById(id) + } + }); + } + + deleteAnnotation(id) { + this.ngbPathwaysService.deleteAnnotationById(id); + } + + refreshAnnotationList() { + this.annotationList = this.ngbPathwaysService.getAnnotationList(); + this.applyAnnotations(); + } + 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 98eb23aba..53fdf2365 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -1,3 +1,5 @@ +$ngb-color-blue: #4285F4; + .ngb-pathway-cytoscape-container { flex: 1; position: relative; @@ -37,7 +39,7 @@ position: absolute; top: 0; right: 15px; - z-index: 1; + z-index: 100; .md-content { display: flex; @@ -52,7 +54,6 @@ box-shadow: 2px 2px 8px 2px #aaa; .pathway-search-panel-body { - display: table; padding-right: 16px; .pathway-search-panel-row { @@ -64,6 +65,32 @@ padding: 5px 0; } + .pathway-search-panel-annotation { + .pathway-search-panel-annotation-checkbox { + margin: 0; + + &:after { + display: block; + height: 0; + width: 0; + } + + md-checkbox { + margin: 0; + } + } + + .pathway-search-panel-annotation-edit { + color: $ngb-color-blue; + } + + .pathway-search-panel-annotation-delete ng-md-icon { + fill: $ngb-color-blue; + cursor: pointer; + align-self: center; + } + } + .pathway-tree-search-input { font-size: 14px; margin: 5px 0 10px 6px; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index 1c210b733..083fc9175 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -32,6 +32,32 @@
+
+ Annotations: + + + Add + + +
+
+
+ + + + +
+ +
+
+ +
+
@@ -42,6 +68,7 @@
{ + if (a.id > result) { + result = a.id; + } + }); + return result; +} + export default class ngbPathwaysService { pathwaysServiceMap = {}; currentInternalPathway; + annotationList = []; + maxAnnotationId = 1; constructor(dispatcher, projectContext, ngbInternalPathwaysTableService, ngbInternalPathwaysResultService @@ -22,6 +34,8 @@ export default class ngbPathwaysService { [PATHWAYS_STATES.INTERNAL_PATHWAYS]: ngbInternalPathwaysTableService, [PATHWAYS_STATES.INTERNAL_PATHWAYS_RESULT]: ngbInternalPathwaysResultService, }; + this.annotationList = JSON.parse(localStorage.getItem('pathwaysAnnotations')) || []; + this.maxAnnotationId = findMaxId(this.annotationList); this.initEvents(); } @@ -50,4 +64,55 @@ export default class ngbPathwaysService { this.currentSearch = data ? data.search : null; }); } + + getAnnotationList() { + return this.annotationList; + } + + saveAnnotationList(list) { + localStorage.setItem('pathwaysAnnotations', JSON.stringify(list)); + } + + getAnnotationById(id) { + const [result] = this.annotationList.filter(a => a.id === id); + return result; + } + + deleteAnnotationById(id) { + const index = this.annotationList.findIndex(a => a.id === id); + if (index > -1) { + this.annotationList.splice(index, 1); + if (this.maxAnnotationId === id) { + this.maxAnnotationId = findMaxId(this.annotationList); + } + } + this.saveAnnotationList(this.annotationList); + this.dispatcher.emitSimpleEvent('pathways:internalPathways:annotations:change'); + } + + setAnnotation(annotation) { + if (annotation.id) { + const index = this.annotationList.findIndex(a => a.id === annotation.id); + if (index > -1) { + this.annotationList[index] = { + ...annotation, + isActive: true + }; + } else { + this.annotationList.push({ + ...annotation, + id: ++this.maxAnnotationId, + isActive: true + }); + } + } else { + this.annotationList.push({ + ...annotation, + id: ++this.maxAnnotationId, + isActive: true + }); + } + this.saveAnnotationList(this.annotationList); + this.dispatcher.emitSimpleEvent('pathways:internalPathways:annotations:change'); + } } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js new file mode 100644 index 000000000..d98a5f748 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js @@ -0,0 +1,80 @@ +function parseConfigStr(rawStr) { + const result = []; + const strList = rawStr.split('\n'); + strList.forEach(str => { + const [term, ...colors] = str.split(/\s+/); + const colorList = colors.join().split(','); + result.push({ + term: term, + backgroundColor: colorList[0] ? colorList[0].trim() : undefined, + foregroundColor: colorList[1] ? colorList[1].trim() : undefined + }); + }); + return result; +} + +function stringifyConfig(config) { + let result = ''; + config.forEach(configStr => { + result += `${configStr.term}\t`; + result += configStr.backgroundColor; + if (configStr.foregroundColor) { + result += `,${configStr.foregroundColor}`; + } + result += '\n'; + }); + return result; +} + +export default class ngbPathwaysAnnotationAddDlgController { + annotationTypeList = { + HEATMAP: 0, + CSV: 1, + MANUAL: 2 + }; + isLoading = false; + configStr = null; + annotation = {}; + + constructor(ngbPathwaysService, $mdDialog, annotation) { + this.ngbPathwaysService = ngbPathwaysService; + this.$mdDialog = $mdDialog; + if (annotation) { + this.annotation = { + id: annotation.id, + name: annotation.name, + type: annotation.type + }; + if (this.annotation.type === this.annotationTypeList.MANUAL) { + this.configStr = stringifyConfig(annotation.value); + } + } else { + this.annotation = { + name: undefined, + type: undefined, + configStr: null + }; + } + } + + static get UID() { + return 'ngbGenesTableDownloadDlgController'; + } + + save() { + let config = []; + if (this.annotation.type === this.annotationTypeList.MANUAL) { + config = parseConfigStr(this.configStr); + } + + this.ngbPathwaysService.setAnnotation({ + ...this.annotation, + value: config + }); + this.close(); + } + + close() { + this.$mdDialog.hide(); + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html new file mode 100644 index 000000000..fe360494b --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html @@ -0,0 +1,51 @@ + + +
+

Add annotation

+ + + + +
+
+ +
+ +
+ + + + +
+ +
+ + + Manual colors config + + + +
+
+ + + Apply + + + + + Cancel + + +
+
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss index 860bddb8d..8c02da3b5 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss @@ -48,3 +48,15 @@ .pathways-search-input .md-errors-spacer { min-height: 0; } + +.add-annotation-dlb-content { + min-width:600px; + max-width: 800px; + margin: 10px 30px; + .md-errors-spacer { + min-height: 0; + } + .add-annotation-divider { + border-top: 2px solid rgba(0,0,0,0.12); + } +} From 4907a305e08abd2f7181caa0471e918d5488543b Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Mon, 7 Feb 2022 15:52:45 +0300 Subject: [PATCH 07/14] Metabolic pathways visualisation (#731): annotations layout fix --- .../ngbCytoscapePathway.controller.js | 3 +- .../ngbCytoscapePathway.settings.js | 5 +++- .../ngbInternalPathwaysResult.scss | 29 +++++++++++++------ .../ngbInternalPathwaysResult.tpl.html | 18 +++++------- .../ngbPathwaysAnnotationAddDlg.tpl.html | 2 +- .../ngbPathways/ngbPathwaysPanel.scss | 8 +++++ 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index 865b4c0e0..13a42a4e3 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -121,6 +121,7 @@ export default class ngbCytoscapePathwayController { style: sbgnStyle, layout: {name: 'preset'}, elements: elements, + ...this.settings.options }); const layout = this.viewer.layout(this.settings.loadedLayout); layout.on('layoutready', () => { @@ -152,7 +153,7 @@ export default class ngbCytoscapePathwayController { layout.run(); const viewerContext = this; this.actionsManager = { - ZOOM_STEP: 0.125, + ZOOM_STEP: viewerContext.settings.externalOptions.zoomStep, duration: 250, zoom: () => viewerContext.viewer.zoom(), zoomIn() { diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js index 8a8341e79..8db433b76 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js @@ -21,7 +21,10 @@ export default { }, options: { wheelSensitivity: 0.25, - minZoom: 0.25, + minZoom: 0.05, maxZoom: 4 + }, + externalOptions: { + zoomStep: 0.05 } }; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss index 53fdf2365..bd3693626 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -38,8 +38,8 @@ $ngb-color-blue: #4285F4; max-width: 300px; position: absolute; top: 0; - right: 15px; - z-index: 100; + right: 50px; + z-index: 1; .md-content { display: flex; @@ -60,14 +60,19 @@ $ngb-color-blue: #4285F4; display: flex; flex-wrap: wrap; align-items: baseline; - border-spacing: 5px; word-break: break-all; padding: 5px 0; + form { + width: 100%; + } + } + .pathway-search-panel-add-annotation span { + font-weight: bold; } - .pathway-search-panel-annotation { .pathway-search-panel-annotation-checkbox { margin: 0; + line-height: 32px; &:after { display: block; @@ -84,16 +89,20 @@ $ngb-color-blue: #4285F4; color: $ngb-color-blue; } - .pathway-search-panel-annotation-delete ng-md-icon { - fill: $ngb-color-blue; - cursor: pointer; - align-self: center; + .pathway-search-panel-annotation-delete { + line-height: 32px; + margin-right: 6px; + ng-md-icon { + fill: $ngb-color-blue; + cursor: pointer; + align-self: center; + } } } .pathway-tree-search-input { font-size: 14px; - margin: 5px 0 10px 6px; + margin: 5px 0 10px 0; } .pathway-tree-search-input .md-errors-spacer { @@ -104,6 +113,8 @@ $ngb-color-blue: #4285F4; line-height: 32px; min-height: 32px; height: 32px; + width: 32px; + min-width: 32px; } } } diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index 083fc9175..ed829e371 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -32,26 +32,24 @@
-
+
Annotations: - Add + +
-
- - - - -
+ + + +
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html index fe360494b..1d0976484 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html @@ -12,7 +12,7 @@

Add annotation

- + diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss index 8c02da3b5..01bbe9308 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss @@ -59,4 +59,12 @@ .add-annotation-divider { border-top: 2px solid rgba(0,0,0,0.12); } + .add-annotation-dlb-name-container { + width: 100%; + padding: 2px 0; + } + .add-annotation-dlb-manual-container { + width: 100%; + padding: 2px 0; + } } From 3dd69b17d8573d82b8b0aed69079ecb4eddcaa30 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Mon, 7 Feb 2022 15:58:32 +0300 Subject: [PATCH 08/14] Metabolic pathways visualisation (#731): add annotations dialog temp layout fix --- .../app/components/ngbPathways/ngbPathwaysPanel.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss index 01bbe9308..29c5c8ca9 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss @@ -60,11 +60,14 @@ border-top: 2px solid rgba(0,0,0,0.12); } .add-annotation-dlb-name-container { - width: 100%; + width: 98%; padding: 2px 0; } .add-annotation-dlb-manual-container { - width: 100%; + width: 98%; padding: 2px 0; + textarea { + padding: 0; + } } } From f293a58ccaf29b1879a47f2a705f646ff9f7c065 Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Tue, 8 Feb 2022 17:15:41 +0300 Subject: [PATCH 09/14] Metabolic pathways visualisation (#731): pathways several layout fixes --- client/client/app/app.layout.constant.js | 1 + .../ngbInternalPathwaysTable.service.js | 4 ++-- .../ngbPathwaysAnnotationAddDlg.tpl.html | 7 ++++--- .../components/ngbPathways/ngbPathwaysPanel.scss | 14 +++++--------- .../ngbToolWindows/ngbToolWindows.controller.js | 2 +- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/client/client/app/app.layout.constant.js b/client/client/app/app.layout.constant.js index 769f8faf8..bdb2fae9b 100644 --- a/client/client/app/app.layout.constant.js +++ b/client/client/app/app.layout.constant.js @@ -89,6 +89,7 @@ export default { pathways: { displayed: false, icon: 'device_hub', + iconColor: 'grey-500', panel: 'ngbPathwaysPanel', position: 'right', title: 'Pathways', diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js index 2e98f366c..b7e512820 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js @@ -8,7 +8,7 @@ const DEFAULT_ORDERBY_INTERNAL_PATHWAYS_COLUMNS = { 'description': 'description' }; const INTERNAL_PATHWAYS_COLUMN_TITLES = { - name: 'Name', + name: 'Map', description: 'Description' }; const FIRST_PAGE = 1; @@ -67,7 +67,7 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer pageSize: this.pageSize, pageNum: this.currentPage }, - sortInfos: this.orderBy, + sortInfo: this.orderBy ? this.orderBy[0] : null, term: currentSearch }; const data = await this.genomeDataService.getInternalPathwaysLoad(filter); diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html index 1d0976484..25fe3d0a5 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html @@ -12,7 +12,7 @@

Add annotation

- + @@ -20,9 +20,10 @@

Add annotation

- - + Manual colors config diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss index 29c5c8ca9..391bceb09 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.scss @@ -59,15 +59,11 @@ .add-annotation-divider { border-top: 2px solid rgba(0,0,0,0.12); } - .add-annotation-dlb-name-container { - width: 98%; - padding: 2px 0; - } - .add-annotation-dlb-manual-container { - width: 98%; - padding: 2px 0; - textarea { - padding: 0; + .add-annotation-dlb-input { + width: 100%; + box-sizing:border-box; + label { + box-sizing:border-box } } } diff --git a/client/client/app/shared/components/ngbMainToolbar/ngbToolWindows/ngbToolWindows.controller.js b/client/client/app/shared/components/ngbMainToolbar/ngbToolWindows/ngbToolWindows.controller.js index 89c288e21..96b6b9e61 100644 --- a/client/client/app/shared/components/ngbMainToolbar/ngbToolWindows/ngbToolWindows.controller.js +++ b/client/client/app/shared/components/ngbMainToolbar/ngbToolWindows/ngbToolWindows.controller.js @@ -39,7 +39,7 @@ export default class ngbToolWindowsController { const panel = Object.assign({}, this.appLayout.Panels[key], {key}); const hotkeys = this.projectContext.hotkeys || this.localDataService.getSettings().hotkeys; panel.displayed = panelsInDispatcher && panelsInDispatcher[panel.panel] === true; - panel.iconColor = this.colors[index++]; + panel.iconColor = panel.iconColor || this.colors[index++]; const objHotkey = hotkeys[panel.name]; if (objHotkey && objHotkey.hotkey) { From 7d20fc801f46aac86e73db74b9ae829a77b2a0ae Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Wed, 9 Feb 2022 15:50:26 +0300 Subject: [PATCH 10/14] Metabolic pathways visualisation (#731): one -> zero annotations switch case --- .../ngbCytoscapePathway/ngbCytoscapePathway.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index 13a42a4e3..64ec1b2f3 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -84,7 +84,7 @@ export default class ngbCytoscapePathwayController { && changes.searchParams.previousValue.search !== changes.searchParams.currentValue.search) { this.searchTree(changes.searchParams.currentValue.search); } - if (changes.searchParams.currentValue.annotations && changes.searchParams.currentValue.annotations.length) { + if (changes.searchParams.currentValue.annotations) { this.annotateTree(changes.searchParams.currentValue.annotations); } } From 9c0d411eb883db8739ae6fbba47f60984a0be29c Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Tue, 22 Feb 2022 18:15:58 +0300 Subject: [PATCH 11/14] Metabolic pathways visualisation (#731): CSV and Heatmap annotations --- .../app/components/ngbPathways/index.js | 7 +- .../ngbPathways/ngbCytoscapePathway/index.js | 5 +- .../ngbCytoscapePathway.controller.js | 128 ++++++++++--- .../ngbInternalPathwaysResult.controller.js | 29 +-- .../ngbInternalPathwaysTable.tpl.html | 2 +- .../ngbPathways/ngbPathways.service.js | 64 ------- .../fileModel.directive.js | 20 ++ .../ngbPathwaysAnnotation/index.js | 15 ++ .../ngbPathwaysAnnotation.component.js | 10 + .../ngbPathwaysAnnotation.controller.js | 158 +++++++++++++++ .../ngbPathwaysAnnotation.scss | 64 +++++++ .../ngbPathwaysAnnotation.service.js | 180 ++++++++++++++++++ .../ngbPathwaysAnnotation.tpl.html | 123 ++++++++++++ .../ngbPathwaysAnnotationAddDlg.controller.js | 80 -------- .../ngbPathwaysAnnotationAddDlg.tpl.html | 52 ----- .../ngbPathwaysAnnotationAddDlg.controller.js | 34 ++++ .../ngbPathwaysAnnotationAddDlg.tpl.html | 27 +++ .../ngbPathwaysColorSchemePreference/index.js | 12 ++ ...PathwaysColorSchemePreference.component.js | 10 + ...PathwaysColorSchemePreference.constants.js | 33 ++++ ...athwaysColorSchemePreference.controller.js | 30 +++ .../ngbPathwaysColorSchemePreference.scss | 69 +++++++ .../ngbPathwaysColorSchemePreference.tpl.html | 155 +++++++++++++++ .../ngbPathways/ngbPathwaysPanel.html | 3 +- .../ngbPathways/ngbPathwaysPanel.scss | 20 +- 25 files changed, 1065 insertions(+), 265 deletions(-) create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/fileModel.directive.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/index.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.component.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.scss create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.tpl.html delete mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js delete mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.tpl.html create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/index.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.component.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.constants.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.controller.js create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.scss create mode 100644 client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.tpl.html diff --git a/client/client/app/components/ngbPathways/index.js b/client/client/app/components/ngbPathways/index.js index c7aec2d5d..29c3a462c 100644 --- a/client/client/app/components/ngbPathways/index.js +++ b/client/client/app/components/ngbPathways/index.js @@ -1,11 +1,12 @@ import angular from 'angular'; import cytoscapePathwayComponent from './ngbCytoscapePathway'; - // Import components import ngbInternalPathwaysResult from './ngbInternalPathwaysResult'; import ngbInternalPathwaysTable from './ngbInternalPathwaysTable'; import service from './ngbPathways.service'; +import ngbPathwaysAnnotation from './ngbPathwaysAnnotation'; +import ngbPathwaysColorSchemePreference from './ngbPathwaysColorSchemePreference'; // Import internal modules import ngbPathwaysPanel from './ngbPathwaysPanel.component'; @@ -21,7 +22,9 @@ export default angular cytoscapePathwayComponent, ngbInternalPathwaysTable, ngbInternalPathwaysResult, - ngbPathwaysPanelPaginate + ngbPathwaysPanelPaginate, + ngbPathwaysColorSchemePreference, + ngbPathwaysAnnotation ]) .service('ngbPathwaysService', service.instance) .component('ngbPathwaysPanel', ngbPathwaysPanel) diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js index b2dc9ed73..3f06a785c 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/index.js @@ -1,15 +1,14 @@ +// Import Style import angular from 'angular'; // Import internal modules import cytoscapeComponent from './ngbCytoscapePathway.component'; import cytoscapeController from './ngbCytoscapePathway.controller'; - -// Import Style import './ngbCytoscapePathway.scss'; import cytoscapeSettings from './ngbCytoscapePathway.settings'; export default angular.module('ngbCytoscapePathway', []) - .constant('cytoscapeSettings', cytoscapeSettings) + .constant('cytoscapePathwaySettings', cytoscapeSettings) .controller(cytoscapeController.UID, cytoscapeController) .component('ngbCytoscapePathway', cytoscapeComponent) .name; diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index 64ec1b2f3..f92b7014a 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -1,4 +1,5 @@ import angular from 'angular'; +import {formatColor} from '../../../../modules/render/heatmap/color-scheme/helpers'; const Cytoscape = require('cytoscape'); const graphml = require('cytoscape-graphml'); @@ -8,16 +9,44 @@ const $ = require('jquery'); const SCALE = 0.3; const searchedColor = '#00cc00'; let defaultNodeStyle = {}; +const annotationTypeList = { + HEATMAP: 0, + CSV: 1, + MANUAL: 2 +}; +const configurationType = { + STRING: 0, + NUMBER: 1, + RANGE: 2 +}; -function deepSearch(obj, term) { +function deepSearch(obj, term, type = configurationType.STRING) { 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 = deepSearch(obj[key], term); } else { - result = obj[key].toString().toLocaleLowerCase() - .includes(term.toLocaleLowerCase()); + switch (type) { + case configurationType.STRING: { + result = obj[key].toString().toLocaleLowerCase() + .includes(term.toLocaleLowerCase()); + break; + } + case configurationType.NUMBER: { + result = Number(obj[key]) === Number(term); + break; + } + case configurationType.RANGE: { + const numericValue = Number(obj[key]); + if (isNaN(numericValue)) { + result = false; + } else { + result = numericValue >= term[0] && numericValue <= term[1]; + } + break; + } + } } if (result) { return true; @@ -28,11 +57,11 @@ function deepSearch(obj, term) { export default class ngbCytoscapePathwayController { - constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapeSettings) { + constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapePathwaySettings) { this.$scope = $scope; this.$compile = $compile; this.cytoscapeContainer = $element.find('.cytoscape-container')[0]; - this.settings = cytoscapeSettings; + this.settings = cytoscapePathwaySettings; this.dispatcher = dispatcher; this.$timeout = $timeout; this.actionsManager = { @@ -128,7 +157,7 @@ export default class ngbCytoscapePathwayController { this.$compile(this.cytoscapeContainer)(this.$scope); this.viewer.on('dragfree', this.saveLayout.bind(this)); this.resizeCytoscape(); - if(this.searchParams.annotations && this.searchParams.annotations.length) { + if (this.searchParams.annotations && this.searchParams.annotations.length) { this.annotateTree(this.searchParams.annotations); } }); @@ -299,32 +328,75 @@ export default class ngbCytoscapePathwayController { } annotateNode(node, annotationList) { - let style = {}; if (node.data('isAnnotated')) { - node.data('isAnnotated', false); - if (!node.data('isFound')) { - style = { - 'background-color': defaultNodeStyle['background-color'], - color: defaultNodeStyle.color, - 'border-color': defaultNodeStyle['border-color'] - }; - node.style(style); - } + this.clearAnnotation(node); } for (const annotation of annotationList) { - for (const termsList of annotation.value) { - if (deepSearch(node.data(), termsList.term) && !node.data('isAnnotated')) { - node.data('isAnnotated', true); - if (!node.data('isFound')) { - style = { - 'background-color': termsList.backgroundColor - }; - if (termsList.foregroundColor) { - style.color = termsList.foregroundColor; - style['border-color'] = termsList.foregroundColor; - } - node.style(style); + if (!node.data('isAnnotated')) { + if (annotation.type === annotationTypeList.MANUAL) { + this.manualAnnotation(node, annotation); + } else { + this.fileAnnotation(node, annotation); + } + } + } + } + + clearAnnotation(node) { + node.data('isAnnotated', false); + if (!node.data('isFound')) { + const style = { + 'background-color': defaultNodeStyle['background-color'], + color: defaultNodeStyle.color, + 'border-color': defaultNodeStyle['border-color'] + }; + node.style(style); + } + } + + manualAnnotation(node, annotation) { + let style; + for (const terms of annotation.value) { + if (deepSearch(node.data(), terms.term)) { + node.data('isAnnotated', true); + if (!node.data('isFound')) { + style = { + 'background-color': terms.backgroundColor + }; + if (terms.foregroundColor) { + style.color = terms.foregroundColor; + style['border-color'] = terms.foregroundColor; } + node.style(style); + } + } + } + } + + fileAnnotation(node, annotation) { + let termsList = []; + let style = {}; + const terms = annotation.value; + + for (let labelIndex = 0; labelIndex < terms.labels.length; labelIndex++) { + if (deepSearch(node.data(), terms.labels[labelIndex])) { + termsList = termsList.concat(terms.values.slice( + labelIndex * terms.blockLength, + (labelIndex + 1) * terms.blockLength + )); + } + const colorList = []; + termsList.forEach(term => { + colorList.push(formatColor(annotation.colorScheme.getColorForValue(term), annotation.colorScheme.colorFormat)); + }); + if (colorList.length) { + node.data('isAnnotated', true); + if (!node.data('isFound')) { + // TODO: colorify to every color after dom_node integration + style = { + 'background-color': colorList[0] + }; + node.style(style); } } } diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js index 8c32433fc..a50662583 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js @@ -1,6 +1,6 @@ import angular from 'angular'; import baseController from '../../../shared/baseController'; -import ngbPathwaysAnnotationAddDlgController from '../ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller'; +import ngbPathwaysAnnotationAddDlgController from '../ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller'; export default class ngbInternalPathwaysResultController extends baseController { selectedTree = null; @@ -24,9 +24,8 @@ export default class ngbInternalPathwaysResultController extends baseController dispatcher, ngbInternalPathwaysResultService, ngbPathwaysService, + ngbPathwaysAnnotationService, appLayout, - projectContext, - localDataService, $mdDialog ) { super(); @@ -39,9 +38,8 @@ export default class ngbInternalPathwaysResultController extends baseController dispatcher, ngbInternalPathwaysResultService, ngbPathwaysService, + ngbPathwaysAnnotationService, appLayout, - projectContext, - localDataService, $mdDialog } ); @@ -82,19 +80,19 @@ export default class ngbInternalPathwaysResultController extends baseController ...this.treeSearchParams, annotations: this.annotationList.filter(a => a.isActive) }; - this.ngbPathwaysService.saveAnnotationList(this.annotationList); + this.ngbPathwaysAnnotationService.saveAnnotationList(this.annotationList); } - // FIXME: separate annotation component addAnnotation() { this.$mdDialog.show({ clickOutsideToClose: true, controller: ngbPathwaysAnnotationAddDlgController, controllerAs: '$ctrl', parent: angular.element(document.body), - template: require('../ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html'), + template: require('../ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.tpl.html'), locals: { - annotation: null + annotation: null, + pathwayId: this.ngbPathwaysService.currentInternalPathway.id } }); } @@ -105,20 +103,23 @@ export default class ngbInternalPathwaysResultController extends baseController controller: ngbPathwaysAnnotationAddDlgController, controllerAs: '$ctrl', parent: angular.element(document.body), - template: require('../ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html'), + template: require('../ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.tpl.html'), locals: { - annotation: this.ngbPathwaysService.getAnnotationById(id) + annotation: this.ngbPathwaysAnnotationService.getAnnotationById(id), + pathwayId: this.ngbPathwaysService.currentInternalPathway.id } }); } deleteAnnotation(id) { - this.ngbPathwaysService.deleteAnnotationById(id); + this.ngbPathwaysAnnotationService.deleteAnnotationById(id); } refreshAnnotationList() { - this.annotationList = this.ngbPathwaysService.getAnnotationList(); - this.applyAnnotations(); + if (this.ngbPathwaysService.currentInternalPathway) { + this.annotationList = this.ngbPathwaysAnnotationService.getAnnotationList(this.ngbPathwaysService.currentInternalPathway.id); + this.applyAnnotations(); + } } activePanelChanged(o) { diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html index ba5bef8e8..e4806b9fd 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.tpl.html @@ -5,7 +5,7 @@
- Nothing found. Try to search another gene. + Nothing found. Try to search by another keywords.
diff --git a/client/client/app/components/ngbPathways/ngbPathways.service.js b/client/client/app/components/ngbPathways/ngbPathways.service.js index b699c0db6..064ff894a 100644 --- a/client/client/app/components/ngbPathways/ngbPathways.service.js +++ b/client/client/app/components/ngbPathways/ngbPathways.service.js @@ -4,21 +4,9 @@ const PATHWAYS_STATES = { INTERNAL_PATHWAYS_RESULT: 'INTERNAL_PATHWAYS_RESULT' }; -function findMaxId(array) { - let result = 0; - array.forEach(a => { - if (a.id > result) { - result = a.id; - } - }); - return result; -} - export default class ngbPathwaysService { pathwaysServiceMap = {}; currentInternalPathway; - annotationList = []; - maxAnnotationId = 1; constructor(dispatcher, projectContext, ngbInternalPathwaysTableService, ngbInternalPathwaysResultService @@ -34,8 +22,6 @@ export default class ngbPathwaysService { [PATHWAYS_STATES.INTERNAL_PATHWAYS]: ngbInternalPathwaysTableService, [PATHWAYS_STATES.INTERNAL_PATHWAYS_RESULT]: ngbInternalPathwaysResultService, }; - this.annotationList = JSON.parse(localStorage.getItem('pathwaysAnnotations')) || []; - this.maxAnnotationId = findMaxId(this.annotationList); this.initEvents(); } @@ -65,54 +51,4 @@ export default class ngbPathwaysService { }); } - getAnnotationList() { - return this.annotationList; - } - - saveAnnotationList(list) { - localStorage.setItem('pathwaysAnnotations', JSON.stringify(list)); - } - - getAnnotationById(id) { - const [result] = this.annotationList.filter(a => a.id === id); - return result; - } - - deleteAnnotationById(id) { - const index = this.annotationList.findIndex(a => a.id === id); - if (index > -1) { - this.annotationList.splice(index, 1); - if (this.maxAnnotationId === id) { - this.maxAnnotationId = findMaxId(this.annotationList); - } - } - this.saveAnnotationList(this.annotationList); - this.dispatcher.emitSimpleEvent('pathways:internalPathways:annotations:change'); - } - - setAnnotation(annotation) { - if (annotation.id) { - const index = this.annotationList.findIndex(a => a.id === annotation.id); - if (index > -1) { - this.annotationList[index] = { - ...annotation, - isActive: true - }; - } else { - this.annotationList.push({ - ...annotation, - id: ++this.maxAnnotationId, - isActive: true - }); - } - } else { - this.annotationList.push({ - ...annotation, - id: ++this.maxAnnotationId, - isActive: true - }); - } - this.saveAnnotationList(this.annotationList); - this.dispatcher.emitSimpleEvent('pathways:internalPathways:annotations:change'); - } } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/fileModel.directive.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/fileModel.directive.js new file mode 100644 index 000000000..2fb7d4a0f --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/fileModel.directive.js @@ -0,0 +1,20 @@ +export default function () { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, elem, attrs, ngModel) { + elem.on('change', function(){ + const reader = new FileReader(); + + reader.addEventListener('load', function (e) { + ngModel.$setViewValue({ + value: e.target.result, + name: elem[0].files[0].name + }); + }); + + reader.readAsBinaryString(elem[0].files[0]); + }); + } + }; +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/index.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/index.js new file mode 100644 index 000000000..15b16d00b --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/index.js @@ -0,0 +1,15 @@ +import angular from 'angular'; +import fileModel from './fileModel.directive'; + +import component from './ngbPathwaysAnnotation.component'; +import controller from './ngbPathwaysAnnotation.controller'; +import './ngbPathwaysAnnotation.scss'; +import service from './ngbPathwaysAnnotation.service'; + +export default angular + .module('ngbPathwaysAnnotation', []) + .service('ngbPathwaysAnnotationService', service) + .directive('fileModel', fileModel) + .controller(controller.UID, controller) + .component('ngbPathwaysAnnotation', component) + .name; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.component.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.component.js new file mode 100644 index 000000000..853de2ee3 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.component.js @@ -0,0 +1,10 @@ +import controller from './ngbPathwaysAnnotation.controller'; + +export default { + bindings: { + annotation: '=' + }, + controller: controller.UID, + restrict: 'E', + template: require('./ngbPathwaysAnnotation.tpl.html'), +}; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js new file mode 100644 index 000000000..357b890cb --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js @@ -0,0 +1,158 @@ +import {HeatmapDataType} from '../../../../modules/render/heatmap'; +import ColorScheme, {ColorFormats} from '../../../../modules/render/heatmap/color-scheme'; +import HeatmapViewOptions from '../../../../modules/render/heatmap/heatmap-view-options'; +import {readHeatmapState} from '../../../../modules/render/heatmap/heatmap-view-options/manager'; + +function initializeManualConfig(config) { + let result = ''; + config.forEach(configStr => { + result += `${configStr.term}\t`; + result += configStr.backgroundColor; + if (configStr.foregroundColor) { + result += `,${configStr.foregroundColor}`; + } + result += '\n'; + }); + return result; +} + +function parseConfigCSV(config) { + let maximum = -Infinity, minimum = Infinity; + let dataType = HeatmapDataType.number; + const cellValues = []; + const splitter = config.indexOf('\r') > -1 ? '\r\n' : '\n'; + const lines = config.split(splitter); + let splitLine = []; + const rowLabels = lines[0].split(','); + const columnLabels = []; + for (let i = 1; i < lines.length; i++) { + splitLine = lines[i].split(','); + columnLabels.push(splitLine.shift()); + for (const val of splitLine) { + const numericValue = Number(val); + if (isNaN(numericValue)) { + cellValues.push(val); + dataType = HeatmapDataType.string; + minimum = undefined; + maximum = undefined; + } else { + cellValues.push(numericValue); + if (numericValue < minimum) { + minimum = numericValue; + } + if (numericValue > maximum) { + maximum = numericValue; + } + } + } + } + return { + columnLabels, rowLabels, + minimum, maximum, + dataType, + cellValues + }; +} + +function heatmapNameSorter(a, b) { + const aName = (a.prettyName || a.name || '').toLowerCase(); + const bName = (b.prettyName || b.name || '').toLowerCase(); + if (!aName) { + return 1; + } + if (!bName) { + return -1; + } + if (aName < bName) { + return -1; + } + if (aName > bName) { + return 1; + } + return 0; +} + +export default class ngbPathwaysAnnotationController { + configurationType = { + STRING: 0, + NUMBER: 1, + RANGE: 2 + }; + annotationFileHeaderList = null; + heatmap = null; + csvFile = {}; + + constructor(ngbPathwaysService, ngbPathwaysAnnotationService, heatmapContext) { + this.heatmapContext = heatmapContext; + this.annotationTypeList = ngbPathwaysAnnotationService.annotationTypeList; + this.annotationFileHeaderList = ngbPathwaysAnnotationService.annotationFileHeaderList; + this.initialize(); + } + + static get UID() { + return 'ngbGenesTableDownloadDlgController'; + } + + get heatmapsFromTrack() { + return this.heatmapContext.heatmaps.filter(heatmap => heatmap.isTrack).sort(heatmapNameSorter); + } + + get heatmapsFromAnnotations() { + return this.heatmapContext.heatmaps.filter(heatmap => heatmap.isAnnotation).sort(heatmapNameSorter); + } + + get heatmapId() { + if (this.heatmap) { + return this.heatmap.id; + } + return undefined; + } + + set heatmapId(id) { + const [heatmap] = this.heatmapContext.heatmaps.filter(h => h.id === id); + this.heatmap = heatmap; + const projectId = this.heatmap.project ? this.heatmap.project.id : this.heatmap.projectIdNumber; + const options = HeatmapViewOptions.parse(readHeatmapState(id)); + options.data.onMetadataLoaded(() => { + this.annotation.colorScheme = options.colorScheme.copy({colorFormat: ColorFormats.hex}); + this.annotation.config = options.data.metadata; + }); + options.data.options = {id, projectId}; + this.annotation.name = this.heatmap.prettyName || this.heatmap.name; + } + + $onChanges(changes) { + if (!!changes.annotation && + !!changes.annotation.currentValue) { + this.initialize(); + } + } + + initialize() { + if (this.annotation) { + if (this.annotation.type === this.annotationTypeList.MANUAL) { + this.annotation.config = initializeManualConfig(this.annotation.value); + } + } + } + + onAnnotationTypeChange() { + this.heatmap = null; + this.csvFile = {}; + const {id, name, type, pathwayId} = this.annotation; + this.annotation = {id, name, type, pathwayId, config: null}; + } + + onFileUpload() { + this.annotation.name = this.csvFile.name; + this.annotation.config = parseConfigCSV(this.csvFile.value); + if (!this.annotation.colorScheme) { + this.annotation.colorScheme = new ColorScheme({ + colorFormat: ColorFormats.hex, + minimum: this.annotation.config.minimum, + maximum: this.annotation.config.maximum, + dataType: this.annotation.config.dataType + }); + } + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.scss b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.scss new file mode 100644 index 000000000..682fbfb9e --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.scss @@ -0,0 +1,64 @@ +.add-annotation-dlb-content { + min-width: 600px; + max-width: 800px; + min-height: 350px; + margin: 10px 30px; + + .md-errors-spacer { + min-height: 0; + } + + .add-annotation-divider { + border-top: 2px solid rgba(0, 0, 0, 0.12); + margin-top: 5px; + margin-bottom: 5px; + } + + .add-annotation-dlb-input { + width: 100%; + box-sizing: border-box; + margin-top: 0; + margin-bottom: 0; + + .md-label { + padding: 0; + } + + label { + box-sizing: border-box + } + + md-checkbox { + margin-bottom: 0; + + .md-label { + line-height: 20px; + } + } + } + + .add-annotation-dlb-file-header { + margin-top: 5px; + + & * { + display: inline-block; + } + + .add-annotation-dlb-file-header-label { + font-size: 12px; + } + } + + .add-annotation-dlb-file { + display: inline-flex; + } + + .add-annotation-dlb-file-checkbox { + line-height: 40px; + } + + .add-annotation-dlb-heatmap { + width: 50%; + min-width: 300px; + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js new file mode 100644 index 000000000..6c77e9603 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js @@ -0,0 +1,180 @@ +import ColorScheme, {ColorFormats} from '../../../../modules/render/heatmap/color-scheme'; + +const ANNOTATION_FILE_HEADER_LIST = { + COLUMN: 0, + ROW: 1 +}; + +const ANNOTATION_TYPE_LIST = { + HEATMAP: 0, + CSV: 1, + MANUAL: 2 +}; + +const ANNOTATION_PREFIX = 'Annotation #'; +const ANNOTATION_STORAGE_NAME = 'pathways-annotations'; + +function findMaxId(array) { + let result = 0; + array.forEach(a => { + if (a.id > result) { + result = a.id; + } + }); + return result; +} + +function parseConfigStr(rawStr) { + const result = []; + const strList = rawStr.split('\n'); + strList.forEach(str => { + const [term, ...colors] = str.split(/\s+/); + const colorList = colors.join().split(','); + result.push({ + term: term, + backgroundColor: colorList[0] ? colorList[0].trim() : undefined, + foregroundColor: colorList[1] ? colorList[1].trim() : undefined + }); + }); + return result; +} + +function prepareConfigCSV(config, header) { + return { + labels: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.columnLabels : config.rowLabels, + blockLength: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.rowLabels.length : config.columnLabels.length, + values: config.cellValues + }; +} + +function parseConfigHeatmap(config, header) { + const labels = config[header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? 'columns' : 'rows'].map(item => item.name); + return { + labels, + blockLength: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.rows.length : config.columns.length, + values: config.values + }; +} + +export default class ngbPathwaysAnnotationService { + annotationList = []; + _maxAnnotationId = 1; + + constructor(dispatcher + ) { + Object.assign( + this, + { + dispatcher + } + ); + + const rawAnnotationList = JSON.parse(localStorage.getItem(ANNOTATION_STORAGE_NAME)) || []; + for (const annotation of rawAnnotationList) { + if (annotation.serializedColorScheme) { + annotation.colorScheme = ColorScheme.parse(annotation.serializedColorScheme).copy({ + colorFormat: ColorFormats.hex + }); + delete annotation.serializedColorScheme; + } + this.annotationList.push(annotation); + } + this._maxAnnotationId = findMaxId(this.annotationList); + } + + get annotationFileHeaderList() { + return ANNOTATION_FILE_HEADER_LIST; + } + + get annotationTypeList() { + return ANNOTATION_TYPE_LIST; + } + + getAnnotationList(pathwayId) { + return this.annotationList.filter(item => item.pathwayId === pathwayId); + } + + saveAnnotationList(list) { + const formattedList = []; + list.filter(item => item.type !== this.annotationTypeList.CSV).forEach(annotation => { + const annotationClone = { + ...annotation + }; + if (annotationClone.colorScheme) { + annotationClone.serializedColorScheme = annotationClone.colorScheme.serialize(); + delete annotationClone.colorScheme; + } + formattedList.push(annotationClone); + }); + localStorage.setItem(ANNOTATION_STORAGE_NAME, JSON.stringify(formattedList)); + } + + getAnnotationById(id) { + const [result] = this.annotationList.filter(a => a.id === id); + return result; + } + + deleteAnnotationById(id) { + const index = this.annotationList.findIndex(a => a.id === id); + if (index > -1) { + this.annotationList.splice(index, 1); + if (this._maxAnnotationId === id) { + this._maxAnnotationId = findMaxId(this.annotationList); + } + } + this.saveAnnotationList(this.annotationList); + this.dispatcher.emitSimpleEvent('pathways:internalPathways:annotations:change'); + } + + setAnnotation(annotation) { + if (annotation.id) { + const index = this.annotationList.findIndex(a => a.id === annotation.id); + if (index > -1) { + this.annotationList[index] = { + ...annotation, + isActive: true + }; + } else { + this.annotationList.push({ + ...annotation, + id: ++this._maxAnnotationId, + isActive: true + }); + } + } else { + if (!annotation.name) { + annotation.name = ANNOTATION_PREFIX + (this._maxAnnotationId + 1); + } + this.annotationList.push({ + ...annotation, + id: ++this._maxAnnotationId, + isActive: true + }); + } + this.saveAnnotationList(this.annotationList); + this.dispatcher.emitSimpleEvent('pathways:internalPathways:annotations:change'); + } + + save(annotation) { + let config = []; + if (annotation.type === this.annotationTypeList.MANUAL) { + config = parseConfigStr(annotation.config); + } + if (annotation.type === this.annotationTypeList.CSV) { + config = prepareConfigCSV(annotation.config, annotation.header); + } + if (annotation.type === this.annotationTypeList.HEATMAP) { + config = parseConfigHeatmap(annotation.config, annotation.header); + } + + if (annotation.colorScheme) { + annotation.colorScheme.initializeFrom(annotation.colorScheme); + } + const savedAnnotation = { + ...annotation, + value: config + }; + delete savedAnnotation.config; + this.setAnnotation(savedAnnotation); + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.tpl.html new file mode 100644 index 000000000..6fb773897 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.tpl.html @@ -0,0 +1,123 @@ +
+
+ + + + +
+ +
+ + + Select heatmap file + + +
+
+ + + + {{heatmapItem.prettyName || heatmapItem.name}} + + + + {{heatmapItem.prettyName || heatmapItem.name}} + + + +
+
+ +
Pathway objects in
+ + first column + + + first row + +
+
+
+ +
+
+ + + Upload TSV/CSV + + +
+ + +
+
+
+ +
Pathway objects in
+ + first column + + + first row + +
+
+
+ +
+ +
+ + + Manual colors config + + +
+
+ + + +
+
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js deleted file mode 100644 index d98a5f748..000000000 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.controller.js +++ /dev/null @@ -1,80 +0,0 @@ -function parseConfigStr(rawStr) { - const result = []; - const strList = rawStr.split('\n'); - strList.forEach(str => { - const [term, ...colors] = str.split(/\s+/); - const colorList = colors.join().split(','); - result.push({ - term: term, - backgroundColor: colorList[0] ? colorList[0].trim() : undefined, - foregroundColor: colorList[1] ? colorList[1].trim() : undefined - }); - }); - return result; -} - -function stringifyConfig(config) { - let result = ''; - config.forEach(configStr => { - result += `${configStr.term}\t`; - result += configStr.backgroundColor; - if (configStr.foregroundColor) { - result += `,${configStr.foregroundColor}`; - } - result += '\n'; - }); - return result; -} - -export default class ngbPathwaysAnnotationAddDlgController { - annotationTypeList = { - HEATMAP: 0, - CSV: 1, - MANUAL: 2 - }; - isLoading = false; - configStr = null; - annotation = {}; - - constructor(ngbPathwaysService, $mdDialog, annotation) { - this.ngbPathwaysService = ngbPathwaysService; - this.$mdDialog = $mdDialog; - if (annotation) { - this.annotation = { - id: annotation.id, - name: annotation.name, - type: annotation.type - }; - if (this.annotation.type === this.annotationTypeList.MANUAL) { - this.configStr = stringifyConfig(annotation.value); - } - } else { - this.annotation = { - name: undefined, - type: undefined, - configStr: null - }; - } - } - - static get UID() { - return 'ngbGenesTableDownloadDlgController'; - } - - save() { - let config = []; - if (this.annotation.type === this.annotationTypeList.MANUAL) { - config = parseConfigStr(this.configStr); - } - - this.ngbPathwaysService.setAnnotation({ - ...this.annotation, - value: config - }); - this.close(); - } - - close() { - this.$mdDialog.hide(); - } -} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html deleted file mode 100644 index 25fe3d0a5..000000000 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotationAddDlg.tpl.html +++ /dev/null @@ -1,52 +0,0 @@ - - -
-

Add annotation

- - - - -
-
- - - -
- - - - -
- -
- - - Manual colors config - - - -
-
- - - Apply - - - - - Cancel - - - -
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js new file mode 100644 index 000000000..d04679ec6 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js @@ -0,0 +1,34 @@ +export default class ngbPathwaysAnnotationAddDlgController { + constructor(ngbPathwaysAnnotationService, $mdDialog, annotation, pathwayId) { + this.ngbPathwaysAnnotationService = ngbPathwaysAnnotationService; + this.$mdDialog = $mdDialog; + if (annotation) { + this.annotation = annotation; + } else { + this.annotation = { + name: undefined, + type: undefined, + config: null, + pathwayId: pathwayId + }; + } + } + + static get UID() { + return 'ngbGenesTableDownloadDlgController'; + } + + get isStateValid() { + return this.annotation.type !== undefined && this.annotation.header !== undefined; + } + + + save() { + this.ngbPathwaysAnnotationService.save(this.annotation); + this.close(); + } + + close() { + this.$mdDialog.hide(); + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.tpl.html new file mode 100644 index 000000000..66a1f9f41 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.tpl.html @@ -0,0 +1,27 @@ + + +
+

Add annotation

+ + + + +
+
+ +
+ + + + + + Apply + + + Cancel + + +
+
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/index.js b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/index.js new file mode 100644 index 000000000..5b2f4c89a --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/index.js @@ -0,0 +1,12 @@ +import angular from 'angular'; +import component from '../ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.component'; +import controller from '../ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.controller'; +import constants from './ngbPathwaysColorSchemePreference.constants'; +import './ngbPathwaysColorSchemePreference.scss'; + +export default angular + .module('ngbPathwaysColorSchemePreference', []) + .constant('ngbPathwaysColorSchemePreferenceConstants', constants) + .controller(controller.UID, controller) + .component('ngbPathwaysColorSchemePreference', component) + .name; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.component.js b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.component.js new file mode 100644 index 000000000..80fca59e2 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.component.js @@ -0,0 +1,10 @@ +import controller from './ngbPathwaysColorSchemePreference.controller'; + +export default { + bindings: { + scheme: '<' + }, + controller: controller.UID, + restrict: 'E', + template: require('./ngbPathwaysColorSchemePreference.tpl.html'), +}; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.constants.js b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.constants.js new file mode 100644 index 000000000..16784e1e9 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.constants.js @@ -0,0 +1,33 @@ +import {ColorFormats, HeatmapColorSchemes, HeatmapDataType} from '../../../../modules/render/heatmap'; + +const colorPickerOptions = { + format: 'hex', + pos: 'bottom right', + swatch: true, + swatchOnly: true, +}; + +export default { + schemes: Object.values(HeatmapColorSchemes), + dataTypes: HeatmapDataType, + colorFormats: ColorFormats, + HeatmapColorSchemes, + schemeName (scheme) { + switch (scheme) { + case HeatmapColorSchemes.discrete: + return 'Discrete'; + case HeatmapColorSchemes.continuous: + default: + return 'Continuous'; + } + }, + colorPickerOptions, + colorPickerOptionsLeft: { + ...colorPickerOptions, + pos: 'bottom right' + }, + colorPickerOptionsRight: { + ...colorPickerOptions, + pos: 'bottom left' + } +}; diff --git a/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.controller.js new file mode 100644 index 000000000..caf608183 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.controller.js @@ -0,0 +1,30 @@ +import baseController from '../../../shared/baseController'; + +export default class ngbPathwaysColorSchemePreferenceController extends baseController { + dispatcher; + isProgressShown = true; + errorMessageList = []; + events = { + }; + + constructor($scope, $timeout, dispatcher, + ngbPathwaysColorSchemePreferenceConstants) { + super(); + + Object.assign(this, { + $scope, + $timeout, + dispatcher, + ngbPathwaysColorSchemePreferenceConstants + }); + + this.scheme = this.scheme || {}; + this.constants = ngbPathwaysColorSchemePreferenceConstants; + + this.initEvents(); + } + + static get UID() { + return 'ngbPathwaysColorSchemePreferenceController'; + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.scss b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.scss new file mode 100644 index 000000000..205cb680d --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.scss @@ -0,0 +1,69 @@ +.color-scheme-preferences { + padding: 10px; +} + +.heatmap-color-pickers { + display: flex; + align-items: center; + justify-content: space-between; +} + +.heatmap-color-picker { + display: inline-flex; + align-items: center; + + color-picker { + display: inline-flex; + } + + .heatmap-color-legend { + margin-left: 5px; + font-size: smaller; + } +} + +.heatmap-config-section { + margin: 5px 0; + padding: 10px 0; + + &:not(:last-child) { + border-bottom: 1px solid #eeeeee; + } +} + +.heatmap-gradient-stop-configuration { + margin: 5px 0; + padding: 5px 0; +} + +.heatmap-gradient-stop-configuration-error { + font-size: small; + color: red; + padding-left: 40px; +} + +.heatmap-color-value-input-container { + margin: 0; + padding: 0; + .md-errors-spacer { + display: none; + } +} + +.heatmap-add-color-button { + margin: 0; +} + +.color-configuration { + width: 100%; + + .color-configuration-value { + flex: 1; + display: flex; + align-items: center; + + md-input-container { + flex: 1; + } + } +} diff --git a/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.tpl.html b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.tpl.html new file mode 100644 index 000000000..dd2b0f713 --- /dev/null +++ b/client/client/app/components/ngbPathways/ngbPathwaysColorSchemePreference/ngbPathwaysColorSchemePreference.tpl.html @@ -0,0 +1,155 @@ +
+
+ + + + + {{$ctrl.constants.schemeName(colorScheme)}} + + + +
+
+
+ + High +
+
+ + Medium +
+
+ + Low +
+
+
+
+
+
+ + Data color +
+
+ +
+ + + + + + + + +
+
+ + + + + + + + + {{value}} + + + +
+ + Range + + + + + +
+
+
+ {{configuration.error}} +
+
+ + ADD COLOR + +
+
+
+ + Missing data +
+
+
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.html b/client/client/app/components/ngbPathways/ngbPathwaysPanel.html index 665618f66..4143f8764 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.html +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.html @@ -1,5 +1,4 @@
-
@@ -33,7 +32,7 @@
- Type gene or feature to start search. + Type keyword to start search.
Date: Thu, 24 Feb 2022 11:54:10 +0300 Subject: [PATCH 12/14] Metabolic pathways visualisation (#731): save to session --- .../ngbBookmarksTable.controller.js | 7 +- .../ngbInternalPathwaysResult.controller.js | 4 +- .../ngbInternalPathwaysResult.scss | 11 ++- .../ngbInternalPathwaysResult.tpl.html | 9 ++- .../ngbPathways/ngbPathways.service.js | 70 +++++++++++++++++-- .../ngbPathwaysAnnotation.controller.js | 2 +- .../ngbPathwaysAnnotation.service.js | 69 +++++++++++------- .../ngbPathwaysAnnotationAddDlg.controller.js | 4 +- .../ngbPathwaysPanel.controller.js | 2 + .../ngbBookmarkSaveDlg.controller.js | 18 ++++- 10 files changed, 155 insertions(+), 41 deletions(-) diff --git a/client/client/app/components/ngbBookmarksPanel/ngbBookmarksTable/ngbBookmarksTable.controller.js b/client/client/app/components/ngbBookmarksPanel/ngbBookmarksTable/ngbBookmarksTable.controller.js index a38308f22..d25c465d0 100644 --- a/client/client/app/components/ngbBookmarksPanel/ngbBookmarksTable/ngbBookmarksTable.controller.js +++ b/client/client/app/components/ngbBookmarksPanel/ngbBookmarksTable/ngbBookmarksTable.controller.js @@ -65,7 +65,8 @@ export default class ngbBookmarksTableController extends baseController { $mdDialog, trackNamingService, appLayout, - ngbStrainLineageService + ngbStrainLineageService, + ngbPathwaysService ) { super(); Object.assign( @@ -81,7 +82,8 @@ export default class ngbBookmarksTableController extends baseController { trackNamingService, miewContext, appLayout, - ngbStrainLineageService + ngbStrainLineageService, + ngbPathwaysService }); this.displayBookmarksFilter = this.ngbBookmarksTableService.displayBookmarksFilter; if (this.projectContext.references.length) { @@ -170,6 +172,7 @@ export default class ngbBookmarksTableController extends baseController { this.miewContext.routeInfo = entity.miew; this.heatmapContext.routeInfo = entity.heatmap; this.ngbStrainLineageService.recoverLocalState(entity.lineage); + this.ngbPathwaysService.recoverLocalState(entity.pathways); this.projectContext.changeState({ chromosome: chromosomeName ? {name: chromosomeName} : undefined, viewport: position, diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js index a50662583..530586d15 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js @@ -43,7 +43,7 @@ export default class ngbInternalPathwaysResultController extends baseController $mdDialog } ); - + this.annotationTypeList = this.ngbPathwaysAnnotationService.annotationTypeList; this.initialize(); this.initEvents(); } @@ -117,7 +117,7 @@ export default class ngbInternalPathwaysResultController extends baseController refreshAnnotationList() { if (this.ngbPathwaysService.currentInternalPathway) { - this.annotationList = this.ngbPathwaysAnnotationService.getAnnotationList(this.ngbPathwaysService.currentInternalPathway.id); + this.annotationList = this.ngbPathwaysAnnotationService.getPathwayAnnotationList(this.ngbPathwaysService.currentInternalPathway.id); this.applyAnnotations(); } } diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss index bd3693626..8d5ed2dca 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -34,8 +34,9 @@ $ngb-color-blue: #4285F4; position: relative; .pathway-search-panel { - width: 50%; + width: 30%; max-width: 300px; + min-width: 180px; position: absolute; top: 0; right: 50px; @@ -85,6 +86,14 @@ $ngb-color-blue: #4285F4; } } + .pathway-search-panel-annotation-name { + flex: 1; + min-width: 70px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .pathway-search-panel-annotation-edit { color: $ngb-color-blue; } diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index ed829e371..2ca837775 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -50,7 +50,14 @@ ng-model="annotation.isActive">
- +
+ {{annotation.name}} + {{annotation.name}} +
+
+ {{annotation.name}} + {{annotation.name}} +
diff --git a/client/client/app/components/ngbPathways/ngbPathways.service.js b/client/client/app/components/ngbPathways/ngbPathways.service.js index 064ff894a..5cd53583f 100644 --- a/client/client/app/components/ngbPathways/ngbPathways.service.js +++ b/client/client/app/components/ngbPathways/ngbPathways.service.js @@ -4,18 +4,21 @@ const PATHWAYS_STATES = { INTERNAL_PATHWAYS_RESULT: 'INTERNAL_PATHWAYS_RESULT' }; +const PATHWAYS_STORAGE_NAME = 'pathwaysState'; + export default class ngbPathwaysService { pathwaysServiceMap = {}; - currentInternalPathway; constructor(dispatcher, projectContext, - ngbInternalPathwaysTableService, ngbInternalPathwaysResultService + ngbInternalPathwaysTableService, ngbInternalPathwaysResultService, + ngbPathwaysAnnotationService ) { Object.assign( this, { dispatcher, - projectContext + projectContext, + ngbPathwaysAnnotationService } ); this.pathwaysServiceMap = { @@ -23,6 +26,7 @@ export default class ngbPathwaysService { [PATHWAYS_STATES.INTERNAL_PATHWAYS_RESULT]: ngbInternalPathwaysResultService, }; this.initEvents(); + this.initState({}); } _currentSearch; @@ -39,10 +43,42 @@ export default class ngbPathwaysService { return PATHWAYS_STATES; } + _currentInternalPathway; + + get currentInternalPathway() { + return this._currentInternalPathway; + } + + set currentInternalPathway(value) { + this._currentInternalPathway = value; + const loadedState = JSON.parse(localStorage.getItem(PATHWAYS_STORAGE_NAME)) || {}; + localStorage.setItem(PATHWAYS_STORAGE_NAME, JSON.stringify({ + ...loadedState, + internalPathway: this._currentInternalPathway + })); + } + + _currentState; + + get currentState() { + return this._currentState; + } + + set currentState(value) { + this._currentState = value; + const loadedState = JSON.parse(localStorage.getItem(PATHWAYS_STORAGE_NAME)) || {}; + localStorage.setItem(PATHWAYS_STORAGE_NAME, JSON.stringify({ + ...loadedState, + state: this._currentState + })); + } + static instance(dispatcher, projectContext, - ngbInternalPathwaysTableService, ngbInternalPathwaysResultService) { + ngbInternalPathwaysTableService, ngbInternalPathwaysResultService, + ngbPathwaysAnnotationService) { return new ngbPathwaysService(dispatcher, projectContext, - ngbInternalPathwaysTableService, ngbInternalPathwaysResultService); + ngbInternalPathwaysTableService, ngbInternalPathwaysResultService, + ngbPathwaysAnnotationService); } initEvents() { @@ -51,4 +87,28 @@ export default class ngbPathwaysService { }); } + initState(loadedState) { + loadedState = { + // TODO: (TBD) do we need to load panel state + // ...JSON.parse(localStorage.getItem(PATHWAYS_STORAGE_NAME)), + ...loadedState + }; + this._currentState = loadedState.state; + this._currentInternalPathway = loadedState.internalPathway; + } + + recoverLocalState(state) { + if (state) { + this.initState(state.layout); + this.ngbPathwaysAnnotationService.initState(state.annotations); + } + } + + getSessionState() { + return { + layout: JSON.parse(localStorage.getItem(PATHWAYS_STORAGE_NAME)), + annotations: this.ngbPathwaysAnnotationService.getSessionAnnotationList() + }; + } + } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js index 357b890cb..d98c5f138 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js @@ -118,7 +118,7 @@ export default class ngbPathwaysAnnotationController { this.annotation.config = options.data.metadata; }); options.data.options = {id, projectId}; - this.annotation.name = this.heatmap.prettyName || this.heatmap.name; + this.annotation.name = this.annotation.name || this.heatmap.prettyName || this.heatmap.name; } $onChanges(changes) { diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js index 6c77e9603..6db0775a3 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js @@ -56,6 +56,21 @@ function parseConfigHeatmap(config, header) { }; } +function prepareAnnotationList(list) { + const result = []; + list.forEach(annotation => { + const annotationClone = { + ...annotation + }; + if (annotationClone.colorScheme) { + annotationClone.serializedColorScheme = annotationClone.colorScheme.serialize(); + delete annotationClone.colorScheme; + } + result.push(annotationClone); + }); + return result; +} + export default class ngbPathwaysAnnotationService { annotationList = []; _maxAnnotationId = 1; @@ -68,18 +83,7 @@ export default class ngbPathwaysAnnotationService { dispatcher } ); - - const rawAnnotationList = JSON.parse(localStorage.getItem(ANNOTATION_STORAGE_NAME)) || []; - for (const annotation of rawAnnotationList) { - if (annotation.serializedColorScheme) { - annotation.colorScheme = ColorScheme.parse(annotation.serializedColorScheme).copy({ - colorFormat: ColorFormats.hex - }); - delete annotation.serializedColorScheme; - } - this.annotationList.push(annotation); - } - this._maxAnnotationId = findMaxId(this.annotationList); + this.initState([]); } get annotationFileHeaderList() { @@ -90,23 +94,21 @@ export default class ngbPathwaysAnnotationService { return ANNOTATION_TYPE_LIST; } - getAnnotationList(pathwayId) { + getSessionAnnotationList() { + return prepareAnnotationList(this.annotationList.filter(item => item.type !== this.annotationTypeList.CSV)); + } + + getPathwayAnnotationList(pathwayId) { return this.annotationList.filter(item => item.pathwayId === pathwayId); } saveAnnotationList(list) { - const formattedList = []; - list.filter(item => item.type !== this.annotationTypeList.CSV).forEach(annotation => { - const annotationClone = { - ...annotation - }; - if (annotationClone.colorScheme) { - annotationClone.serializedColorScheme = annotationClone.colorScheme.serialize(); - delete annotationClone.colorScheme; - } - formattedList.push(annotationClone); - }); - localStorage.setItem(ANNOTATION_STORAGE_NAME, JSON.stringify(formattedList)); + localStorage.setItem( + ANNOTATION_STORAGE_NAME, + JSON.stringify(prepareAnnotationList( + list.filter(item => item.type !== this.annotationTypeList.CSV) + )) + ); } getAnnotationById(id) { @@ -177,4 +179,21 @@ export default class ngbPathwaysAnnotationService { delete savedAnnotation.config; this.setAnnotation(savedAnnotation); } + + initState(annotationList) { + const rawAnnotationList = annotationList && annotationList.length + ? annotationList + : JSON.parse(localStorage.getItem(ANNOTATION_STORAGE_NAME)) || []; + this.annotationList = []; + for (const annotation of rawAnnotationList) { + if (annotation.serializedColorScheme) { + annotation.colorScheme = ColorScheme.parse(annotation.serializedColorScheme).copy({ + colorFormat: ColorFormats.hex + }); + delete annotation.serializedColorScheme; + } + this.annotationList.push(annotation); + } + this._maxAnnotationId = findMaxId(this.annotationList); + } } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js index d04679ec6..528219c55 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotationDlg/ngbPathwaysAnnotationAddDlg.controller.js @@ -19,7 +19,9 @@ export default class ngbPathwaysAnnotationAddDlgController { } get isStateValid() { - return this.annotation.type !== undefined && this.annotation.header !== undefined; + return this.annotation.type !== undefined + && (this.annotation.type === this.ngbPathwaysAnnotationService.annotationTypeList.MANUAL + || this.annotation.header !== undefined); } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js index e4db9b594..97f56637c 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysPanel.controller.js @@ -24,6 +24,7 @@ export default class ngbPathwaysPanelController extends baseController { }); this.pathwaysStates = this.ngbPathwaysService.pathwaysStates; this.initEvents(); + this.changeState(this.ngbPathwaysService.currentState); } static get UID() { @@ -41,6 +42,7 @@ export default class ngbPathwaysPanelController extends baseController { break; } } + this.ngbPathwaysService.currentState = state; } this.$timeout(() => this.$scope.$apply()); } diff --git a/client/client/app/shared/components/ngbMainToolbar/ngbBookmark/ngbBookmarkSaveDlg.controller.js b/client/client/app/shared/components/ngbMainToolbar/ngbBookmark/ngbBookmarkSaveDlg.controller.js index c1a44cae6..10bfbf2d5 100644 --- a/client/client/app/shared/components/ngbMainToolbar/ngbBookmark/ngbBookmarkSaveDlg.controller.js +++ b/client/client/app/shared/components/ngbMainToolbar/ngbBookmark/ngbBookmarkSaveDlg.controller.js @@ -14,6 +14,7 @@ export default class ngbBookmarkSaveDlgController { $scope, trackNamingService, ngbStrainLineageService, + ngbPathwaysService, bookmarkDataService ) { Object.assign(this, { @@ -25,7 +26,8 @@ export default class ngbBookmarkSaveDlgController { heatmapContext, trackNamingService, bookmarkDataService, - ngbStrainLineageService + ngbStrainLineageService, + ngbPathwaysService }); } @@ -64,6 +66,13 @@ export default class ngbBookmarkSaveDlgController { return undefined; } + getPathwaysState() { + if (this.ngbPathwaysService) { + return this.ngbPathwaysService.getSessionState(); + } + return undefined; + } + save(event) { this.isLoading = true; const ruler = this.projectContext.viewport @@ -93,7 +102,8 @@ export default class ngbBookmarkSaveDlgController { customNames, this.miewContext.routeInfo, this.heatmapContext.routeInfo, - this.getLineageState() + this.getLineageState(), + this.getPathwaysState() ); const params = { owner: '', @@ -137,7 +147,8 @@ function Bookmark( customNames, miew, heatmap, - lineage + lineage, + pathways ) { this.name = name; this.description = description; @@ -174,4 +185,5 @@ function Bookmark( this.miew = miew; this.heatmap = heatmap; this.lineage = lineage; + this.pathways = pathways; } From 5167cbd9727081ae96d48bc88aa2a9bb6a0f944c Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Thu, 24 Feb 2022 15:51:26 +0300 Subject: [PATCH 13/14] Metabolic pathways visualisation (#731): row/column for heatmap --- .../ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss | 2 +- .../ngbInternalPathwaysResult.tpl.html | 1 - .../ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss index 8d5ed2dca..f54672fd4 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss @@ -35,7 +35,7 @@ $ngb-color-blue: #4285F4; .pathway-search-panel { width: 30%; - max-width: 300px; + max-width: 250px; min-width: 180px; position: absolute; top: 0; diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index 2ca837775..613d8db7d 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -58,7 +58,6 @@ {{annotation.name}} {{annotation.name}}
-
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js index 6db0775a3..06bff9c78 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js @@ -48,10 +48,10 @@ function prepareConfigCSV(config, header) { } function parseConfigHeatmap(config, header) { - const labels = config[header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? 'columns' : 'rows'].map(item => item.name); + const labels = config[header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? 'rows' : 'columns'].map(item => item.name); return { labels, - blockLength: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.rows.length : config.columns.length, + blockLength: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.columns.length : config.rows.length, values: config.values }; } From d5cacec5c4729b745b59677585266924b9ba8a6d Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Thu, 24 Feb 2022 18:31:21 +0300 Subject: [PATCH 14/14] Metabolic pathways visualisation (#731): edit heatmap, heatmap parse logic changed --- .../ngbCytoscapePathway.controller.js | 5 +-- .../ngbInternalPathwaysResult.tpl.html | 4 +-- .../ngbPathwaysAnnotation.controller.js | 32 ++++++++++++++++--- .../ngbPathwaysAnnotation.service.js | 21 ++++++------ 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js index f92b7014a..7d60833fd 100644 --- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js +++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js @@ -380,10 +380,7 @@ export default class ngbCytoscapePathwayController { for (let labelIndex = 0; labelIndex < terms.labels.length; labelIndex++) { if (deepSearch(node.data(), terms.labels[labelIndex])) { - termsList = termsList.concat(terms.values.slice( - labelIndex * terms.blockLength, - (labelIndex + 1) * terms.blockLength - )); + termsList = termsList.concat(terms.values[labelIndex]); } const colorList = []; termsList.forEach(term => { diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html index 613d8db7d..a9c23b31f 100644 --- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html +++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html @@ -50,11 +50,11 @@ ng-model="annotation.isActive"> -
+
{{annotation.name}} {{annotation.name}}
-
+
{{annotation.name}} {{annotation.name}}
diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js index d98c5f138..e4b7777f2 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.controller.js @@ -26,17 +26,19 @@ function parseConfigCSV(config) { const rowLabels = lines[0].split(','); const columnLabels = []; for (let i = 1; i < lines.length; i++) { + if (!lines[i]) { + continue; + } splitLine = lines[i].split(','); columnLabels.push(splitLine.shift()); + cellValues.push(splitLine); for (const val of splitLine) { const numericValue = Number(val); if (isNaN(numericValue)) { - cellValues.push(val); dataType = HeatmapDataType.string; minimum = undefined; maximum = undefined; } else { - cellValues.push(numericValue); if (numericValue < minimum) { minimum = numericValue; } @@ -113,9 +115,26 @@ export default class ngbPathwaysAnnotationController { this.heatmap = heatmap; const projectId = this.heatmap.project ? this.heatmap.project.id : this.heatmap.projectIdNumber; const options = HeatmapViewOptions.parse(readHeatmapState(id)); - options.data.onMetadataLoaded(() => { - this.annotation.colorScheme = options.colorScheme.copy({colorFormat: ColorFormats.hex}); - this.annotation.config = options.data.metadata; + options.data.onDataLoaded(payload => { + const values = []; + for (const heatmapDataItem of payload.data.entries()) { + if (!values[heatmapDataItem.row]) { + values[heatmapDataItem.row] = []; + } + values[heatmapDataItem.row][heatmapDataItem.column] = heatmapDataItem.value; + } + this.annotation.config = { + rows: payload.metadata.rows, + columns: payload.metadata.columns, + heatmapId: id, + values + }; + if (!this.annotation.colorScheme) { + const colorSchemeParams = {colorFormat: ColorFormats.hex}; + colorSchemeParams.minimum = this.annotation.config.minCellValue; + colorSchemeParams.maximum = this.annotation.config.maxCellValue; + this.annotation.colorScheme = options.colorScheme.copy(colorSchemeParams); + } }); options.data.options = {id, projectId}; this.annotation.name = this.annotation.name || this.heatmap.prettyName || this.heatmap.name; @@ -133,6 +152,9 @@ export default class ngbPathwaysAnnotationController { if (this.annotation.type === this.annotationTypeList.MANUAL) { this.annotation.config = initializeManualConfig(this.annotation.value); } + if (this.annotation.type === this.annotationTypeList.HEATMAP) { + this.heatmapId = this.annotation.value.heatmapId; + } } } diff --git a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js index 06bff9c78..d275229d0 100644 --- a/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js +++ b/client/client/app/components/ngbPathways/ngbPathwaysAnnotation/ngbPathwaysAnnotation.service.js @@ -28,13 +28,15 @@ function parseConfigStr(rawStr) { const result = []; const strList = rawStr.split('\n'); strList.forEach(str => { - const [term, ...colors] = str.split(/\s+/); - const colorList = colors.join().split(','); - result.push({ - term: term, - backgroundColor: colorList[0] ? colorList[0].trim() : undefined, - foregroundColor: colorList[1] ? colorList[1].trim() : undefined - }); + if (str) { + const [term, ...colors] = str.split(/\s+/); + const colorList = colors.join().split(','); + result.push({ + term: term, + backgroundColor: colorList[0] ? colorList[0].trim() : undefined, + foregroundColor: colorList[1] ? colorList[1].trim() : undefined + }); + } }); return result; } @@ -42,7 +44,6 @@ function parseConfigStr(rawStr) { function prepareConfigCSV(config, header) { return { labels: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.columnLabels : config.rowLabels, - blockLength: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.rowLabels.length : config.columnLabels.length, values: config.cellValues }; } @@ -51,8 +52,8 @@ function parseConfigHeatmap(config, header) { const labels = config[header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? 'rows' : 'columns'].map(item => item.name); return { labels, - blockLength: header === ANNOTATION_FILE_HEADER_LIST.COLUMN ? config.columns.length : config.rows.length, - values: config.values + values: config.values, + heatmapId: config.heatmapId }; }