From 2a628f9e1119d1557cf4dfd1bc4fab7af566150a Mon Sep 17 00:00:00 2001 From: Dmitrii Krasnov Date: Wed, 26 Jan 2022 20:05:38 +0300 Subject: [PATCH] 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]'},