From 79c9995ab163d45223bc8f8b7458673cc297ecb6 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 15 Feb 2021 08:07:47 -0800 Subject: [PATCH 01/80] #984 selection of Term Context node triggers addVFBID and changes UI, but doesn't update the Term Context component unless user presses sync button --- components/interface/VFBGraph/VFBGraph.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/interface/VFBGraph/VFBGraph.js b/components/interface/VFBGraph/VFBGraph.js index 22fc7f914..e632aa4ca 100644 --- a/components/interface/VFBGraph/VFBGraph.js +++ b/components/interface/VFBGraph/VFBGraph.js @@ -266,8 +266,7 @@ class VFBGraph extends Component { * Handle Left click on Nodes */ handleNodeLeftClick (node, event) { - this.nodeSelectedID = node.title; - this.queryNewInstance(node); + window.addVfbId(node.title) } /** From a3ed21c05cd13d6d69d289808e16313346c0a87d Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 16 Feb 2021 13:42:04 -0800 Subject: [PATCH 02/80] #987 Update query for Circuit Browser. Makes Circuit Browser component visible. --- components/VFBMain.js | 8 ++++++-- .../circuitBrowserConfiguration.js | 18 ++++++++++++------ .../configuration/VFBMain/layoutModel.js | 5 +++++ .../VFBToolbar/vfbtoolbarMenuConfiguration.js | 8 ++++++++ .../VFBCircuitBrowser/VFBCircuitBrowser.js | 5 +++-- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/components/VFBMain.js b/components/VFBMain.js index f347583c3..832aa05a1 100644 --- a/components/VFBMain.js +++ b/components/VFBMain.js @@ -37,7 +37,7 @@ class VFBMain extends React.Component { canvasVisible: true, listViewerVisible: true, graphVisible : true, - circuitBrowserVisible : false, + circuitBrowserVisible : true, htmlFromToolbar: undefined, idSelected: undefined, instanceOnFocus: undefined, @@ -1022,7 +1022,11 @@ class VFBMain extends React.Component { } else if (component === "vfbCircuitBrowser") { let circuitBrowserVisibility = node.isVisible(); node.setEventListener("close", () => { - self.props.vfbCircuitBrowser(ACTIONS.UPDATE_CIRCUIT_QUERY,null,false); + self.setState({ + UIUpdated: true, + circuitBrowserVisible: false + }); + self.props.vfbCircuitBrowser(ACTIONS.UPDATE_CIRCUIT_QUERY,null,false); }); // Event listener fired when circuit browser component is resized diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 35b522323..684d4513e 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -1,10 +1,16 @@ -var locationCypherQuery = ( instances, hops ) => ({ - "statements": [ +var locationCypherQuery = ( instances, hops , weight ) => ({ + statements: [ { - "statement" : "WITH [" + instances + "] AS neurons" - + " MATCH p=(x:Class)-[:synapsed_to*.." + hops.toString() + "]->(y:Class)" - + " WHERE x.short_form in neurons and y.short_form in neurons" - + " RETURN p, neurons", + "statement" : "WITH [" + instances + "] AS neurons" + + " WITH neurons[0] as root, neurons[1..] AS neurons" + + " MATCH p=(x:Neuron {short_form: root})-[:synapsed_to*.." + hops.toString() + "]->(y:Neuron)" + + " WHERE y.short_form IN neurons AND" + + " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")" + + " WITH root, relationships(p) as fu" + + " UNWIND fu as r" + + " WITH root, startNode(r) AS a, endNode(r) AS b" + + " MATCH p=(a)-[:synapsed_to]-(b)" + + " RETURN p, root", "resultDataContents": ["graph"] } ] diff --git a/components/configuration/VFBMain/layoutModel.js b/components/configuration/VFBMain/layoutModel.js index 5f9afe8bd..907b68bb5 100644 --- a/components/configuration/VFBMain/layoutModel.js +++ b/components/configuration/VFBMain/layoutModel.js @@ -71,6 +71,11 @@ var modelJson = { "type": "tab", "name": "Layers", "component": "vfbListViewer" + }, + { + "type": "tab", + "name": "Circuit Browser", + "component": "vfbCircuitBrowser" } ] } diff --git a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js index 0412122a8..67169b050 100644 --- a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js +++ b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js @@ -245,6 +245,14 @@ var toolbarMenu = { parameters: ["graphVisible"] } }, + { + label: "Circuit Browser", + icon: "fa fa-cogs", + action: { + handlerAction: "UIElementHandler", + parameters: ["circuitBrowserVisible"] + } + }, { label: "NBLAST", icon: "", diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 81273dcb5..bc92a2aa8 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -181,8 +181,8 @@ class VFBCircuitBrowser extends Component { if (this.__isMounted){ // Show loading spinner while cypher query search occurs this.setState({ loading : true , neurons : neurons, hops : hops, queryLoaded : false }); - // Perform cypher query - this.queryResults(cypherQuery(neurons.map(d => `'${d}'`).join(','), hops)); + // Perform cypher query. TODO: Remove hardcoded weight once edge weight is implemented + this.queryResults(cypherQuery(neurons.map(d => `'${d}'`).join(','), hops, 70)); } } @@ -352,6 +352,7 @@ class VFBCircuitBrowser extends Component { // bu = Bottom Up, creates Graph with root at bottom dagMode="lr" dagLevelDistance = {100} + onDagError={loopNodeIds => {}} // Handles clicking event on an individual node onNodeClick = { (node,event) => this.handleNodeLeftClick(node,event) } ref={this.graphRef} From 56eecc21a383cc6d2b7d217585d826204c991d5e Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 17 Feb 2021 10:52:01 -0800 Subject: [PATCH 03/80] #987 Fix eslint issues --- components/VFBMain.js | 4 ++-- .../VFBCircuitBrowser/circuitBrowserConfiguration.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/VFBMain.js b/components/VFBMain.js index 832aa05a1..5b122dbe0 100644 --- a/components/VFBMain.js +++ b/components/VFBMain.js @@ -1022,11 +1022,11 @@ class VFBMain extends React.Component { } else if (component === "vfbCircuitBrowser") { let circuitBrowserVisibility = node.isVisible(); node.setEventListener("close", () => { - self.setState({ + self.setState({ UIUpdated: true, circuitBrowserVisible: false }); - self.props.vfbCircuitBrowser(ACTIONS.UPDATE_CIRCUIT_QUERY,null,false); + self.props.vfbCircuitBrowser(ACTIONS.UPDATE_CIRCUIT_QUERY,null,false); }); // Event listener fired when circuit browser component is resized diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 684d4513e..fdd2832e8 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -1,5 +1,5 @@ var locationCypherQuery = ( instances, hops , weight ) => ({ - statements: [ + statements: [ { "statement" : "WITH [" + instances + "] AS neurons" + " WITH neurons[0] as root, neurons[1..] AS neurons" From 34a67bc5c14a7d8a91efd11b33818e874b7c9a01 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 19 Feb 2021 15:38:41 -0800 Subject: [PATCH 04/80] #985 Plugs in SOLR to Circuit Browser search fields, and selection of instances is done by instance name (#986) --- .../interface/VFBCircuitBrowser/Controls.js | 105 ++++++++++++------ .../VFBCircuitBrowser/VFBCircuitBrowser.js | 18 +-- package.json | 1 + 3 files changed, 85 insertions(+), 39 deletions(-) diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index 16dbb0323..9ca823c44 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -3,6 +3,7 @@ import Accordion from '@material-ui/core/Accordion'; import AccordionDetails from '@material-ui/core/AccordionDetails'; import AccordionSummary from '@material-ui/core/AccordionSummary'; import AccordionActions from '@material-ui/core/AccordionActions'; +import Autocomplete from '@material-ui/lab/Autocomplete'; import Typography from '@material-ui/core/Typography'; import Chip from '@material-ui/core/Chip'; import Divider from '@material-ui/core/Divider'; @@ -24,6 +25,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { connect } from "react-redux"; import { UPDATE_CIRCUIT_QUERY } from './../../../actions/generals'; +import { DatasourceTypes } from '@geppettoengine/geppetto-ui/search/datasources/datasources'; +import { getResultsSOLR } from "@geppettoengine/geppetto-ui/search/datasources/SOLRclient"; /** * Create a local theme to override some default values in material-ui components @@ -97,6 +100,9 @@ const restPostConfig = require('../../configuration/VFBCircuitBrowser/circuitBro const cypherQuery = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').locationCypherQuery; const stylingConfiguration = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').styling; +const searchConfiguration = require('./../../configuration/VFBMain/searchConfiguration').searchConfiguration; +const datasourceConfiguration = require('./../../configuration/VFBMain/searchConfiguration').datasourceConfiguration; + /** * Create custom marks for Hops slider. * Only show the label for the minimum and maximum hop, hide the rest @@ -124,7 +130,8 @@ class Controls extends Component { this.state = { typingTimeout: 0, expanded : true, - neuronFields : ["", ""] + neuronFields : [{ id : "", label : "" } , { id : "", label : "" }], + filteredResults : {} }; this.addNeuron = this.addNeuron.bind(this); this.neuronTextfieldModified = this.neuronTextfieldModified.bind(this); @@ -133,6 +140,8 @@ class Controls extends Component { this.fieldsValidated = this.fieldsValidated.bind(this); this.deleteNeuronField = this.deleteNeuronField.bind(this); this.getUpdatedNeuronFields = this.getUpdatedNeuronFields.bind(this); + this.handleResults = this.handleResults.bind(this); + this.resultSelectedChanged = this.resultSelectedChanged.bind(this); this.circuitQuerySelected = this.props.circuitQuerySelected; } @@ -140,6 +149,7 @@ class Controls extends Component { let neurons = [...this.props.neurons]; this.setState( { expanded : !this.props.resultsAvailable(), neuronFields : neurons } ); this.circuitQuerySelected = this.props.circuitQuerySelected; + this.setInputValue = {}; } componentDidUpdate () {} @@ -159,7 +169,7 @@ class Controls extends Component { this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); - // Update state with one less neuron textfield + // Update state with one fewer neuron textfield this.setState( { neuronFields : neurons } ); // If neuron fields are validated, let the VFBCircuitBrowser component know, it will do a graph update @@ -174,7 +184,7 @@ class Controls extends Component { addNeuron () { let neuronFields = this.state.neuronFields; // Add emptry string for now to text field - neuronFields.push(""); + neuronFields.push({ id : "", label : "" }); // User has added the maximum number of neurons allowed in query search if ( configuration.maxNeurons <= neuronFields.length ) { this.setState({ neuronFields : neuronFields }); @@ -189,9 +199,9 @@ class Controls extends Component { fieldsValidated (neurons) { var pattern = /^[a-zA-Z0-9].*_[a-zA-Z0-9]{8}$/; for ( var i = 0 ; i < neurons.length ; i++ ){ - if ( neurons[i] === "" ) { + if ( neurons?.[i].id == "" ) { return false; - } else if ( !neurons[i].match(pattern) ) { + } else if ( !neurons?.[i].id?.match(pattern) ) { return false; } } @@ -199,33 +209,50 @@ class Controls extends Component { return true; } + handleResults (status, data, value) { + let results = {}; + data?.map(result => { + if ( result?.short_form?.toLowerCase().includes(value?.toLowerCase()) ){ + results[result?.label] = result; + } else if ( result?.label?.toLowerCase().includes(value?.toLowerCase()) ){ + results[result?.label] = result; + } + }); + + this.setState({ filteredResults : results }) + } + /** * Waits some time before performing new search, this to avoid performing search everytime * enters a new character in neuron fields */ typingTimeout (target) { - let neurons = this.state.neuronFields; - neurons[target.id] = target.value; - this.circuitQuerySelected = neurons; - this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); - if ( this.fieldsValidated(neurons) ) { - this.setState( { neuronFields : neurons } ); - this.props.queriesUpdated(neurons); - } + this.setInputValue = target.id; + getResultsSOLR( target.value, this.handleResults,searchConfiguration.sorter,datasourceConfiguration ); } /** * Neuron text field has been modified. */ neuronTextfieldModified (event) { - const self = this; + this.resultsHeight = event.target.offsetTop + 15; // Remove old typing timeout interval - if (self.state.typingTimeout) { + if (this.state.typingTimeout) { clearTimeout(this.typingTimeout); } - let target = event.target; // Create a setTimeout interval, to avoid performing searches on every stroke - setTimeout(this.typingTimeout, 500, target); + setTimeout(this.typingTimeout, 500, event.target); + } + + resultSelectedChanged (event, value) { + let neurons = this.state.neuronFields; + neurons[this.setInputValue] = { id : this.state.filteredResults?.[value].short_form, label : value }; + this.circuitQuerySelected = neurons; + this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); + if ( this.fieldsValidated(neurons) ) { + this.setState( { neuronFields : neurons } ); + this.props.queriesUpdated(neurons); + } } /** @@ -245,18 +272,22 @@ class Controls extends Component { let neuronFields = this.state.neuronFields; let added = false; for ( var i = 0; i < this.props.circuitQuerySelected.length; i++ ){ - if ( !this.state.neuronFields.includes(this.props.circuitQuerySelected[i])) { + var fieldExists = this.state.neuronFields.filter(entry => + entry.id === this.props.circuitQuerySelected[i] + ); + + if ( !fieldExists) { for ( var j = 0 ; j < neuronFields.length ; j++ ) { - if ( this.state.neuronFields[j] === "" ) { - neuronFields[j] = this.props.circuitQuerySelected[i]; + if ( this.state.neuronFields?.[j].id === "" ) { + neuronFields[j] = { id : this.props.circuitQuerySelected[i], label : "test" }; added = true; break; } } - if ( this.props.circuitQuerySelected.length > neuronFields.length && !this.state.neuronFields.includes(this.circuitQuerySelected[i])) { + if ( this.props.circuitQuerySelected.length > neuronFields.length && !fieldExists) { if ( neuronFields.length < configuration.maxNeurons && this.props.circuitQuerySelected !== "" ) { - neuronFields.push(this.props.circuitQuerySelected[i]); + neuronFields.push({ id : this.props.circuitQuerySelected[i], label : "test" }); } } } @@ -325,21 +356,31 @@ class Controls extends Component { - { neuronFields.map((value, index) => { + { neuronFields.map((field, index) => { let label = "Neuron " + (index + 1) .toString(); return - + onChange={this.resultSelectedChanged} + options={Object.keys(this.state.filteredResults).map(option => this.state.filteredResults[option].label)} + renderInput={params => ( + + )} + /> + { deleteIconVisible ? `'${d}'`).join(','), hops, 70)); + this.queryResults(cypherQuery(neurons.map(a => `'${a.id}'`).join(","), hops, 70)); } } @@ -268,7 +273,7 @@ class VFBCircuitBrowser extends Component { this.circuitQuerySelected = circuitQuerySelected; let errorMessage = "Not enough input queries to create a graph, needs 2."; - if ( this.state.neurons[0] != "" && this.state.neurons[1] != "" ){ + if ( this.state.neurons?.[0].id != "" && this.state.neurons?.[1].id != "" ){ errorMessage = "Graph not available for " + this.state.neurons.join(","); } return ( @@ -374,7 +379,6 @@ class VFBCircuitBrowser extends Component { zoomIn={self.zoomIn} zoomOut={self.zoomOut} circuitQuerySelected={this.circuitQuerySelected} - datasource="SOLR" legend = {self.state.legend} /> } diff --git a/package.json b/package.json index f7eb95e49..a64b64f55 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@babel/plugin-transform-runtime": "^7.4.5", "@geppettoengine/geppetto-client": "file:./geppetto-client", "@material-ui/icons": "3.0.1", + "@material-ui/lab": "^4.0.0-alpha.57", "@types/react-rnd": "^8.0.0", "axios": "^0.19.2", "babel-loader": "^8.0.6", From fd4e2a009b5a93ded643162870996a03c12ae2d4 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 19 Feb 2021 16:12:01 -0800 Subject: [PATCH 05/80] #985 Clean code --- .../interface/VFBCircuitBrowser/Controls.js | 16 ++++++++++++++-- .../VFBCircuitBrowser/VFBCircuitBrowser.js | 8 +------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index 9ca823c44..90977918e 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -209,9 +209,13 @@ class Controls extends Component { return true; } + /** + * Receives SOLR results and creates an map with those results that match the input text + */ handleResults (status, data, value) { let results = {}; data?.map(result => { + // Match results by short_form id if ( result?.short_form?.toLowerCase().includes(value?.toLowerCase()) ){ results[result?.label] = result; } else if ( result?.label?.toLowerCase().includes(value?.toLowerCase()) ){ @@ -244,11 +248,19 @@ class Controls extends Component { setTimeout(this.typingTimeout, 500, event.target); } + /** + * Handle SOLR result selection, activated by selecting from drop down menu under textfield + */ resultSelectedChanged (event, value) { + // Copy neurons and add selection to correct array index let neurons = this.state.neuronFields; neurons[this.setInputValue] = { id : this.state.filteredResults?.[value].short_form, label : value }; + + // Keep track of query selected, and send an event to redux store that circuit has been updated this.circuitQuerySelected = neurons; this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); + + // If text fields contain valid ids, perform query if ( this.fieldsValidated(neurons) ) { this.setState( { neuronFields : neurons } ); this.props.queriesUpdated(neurons); @@ -279,7 +291,7 @@ class Controls extends Component { if ( !fieldExists) { for ( var j = 0 ; j < neuronFields.length ; j++ ) { if ( this.state.neuronFields?.[j].id === "" ) { - neuronFields[j] = { id : this.props.circuitQuerySelected[i], label : "test" }; + neuronFields[j] = { id : this.props.circuitQuerySelected[i], label : "" }; added = true; break; } @@ -287,7 +299,7 @@ class Controls extends Component { if ( this.props.circuitQuerySelected.length > neuronFields.length && !fieldExists) { if ( neuronFields.length < configuration.maxNeurons && this.props.circuitQuerySelected !== "" ) { - neuronFields.push({ id : this.props.circuitQuerySelected[i], label : "test" }); + neuronFields.push({ id : this.props.circuitQuerySelected[i], label : "" }); } } } diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index b37b9401e..23b49b99d 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -119,14 +119,9 @@ class VFBCircuitBrowser extends Component { queriesUpdated (neurons) { // Check if new list of neurons is the same as the ones already rendered on last update var matched = (this.state.neurons.length == neurons.length) && this.state.neurons.every(function (element, index) { - console.log("1st " + element.id); - console.log("2nd " + neurons[index].id); return element.id === neurons[index].id; }); - console.log("Is same " + matched); - console.log("this.state.loading " + this.state.loading); - // Request graph update if the list of new neurons is not the same if ( !this.state.loading && !matched ) { this.updateGraph(neurons, this.state.hops); @@ -238,7 +233,6 @@ class VFBCircuitBrowser extends Component { worker.postMessage({ message: "refine", params: { results: response.data, configuration : configuration, styling : stylingConfiguration, NODE_WIDTH : NODE_WIDTH, NODE_HEIGHT : NODE_HEIGHT } }); }) .catch( function (error) { - console.log("HTTP Request Error: ", error); self.setState( { loading : false } ); }) } @@ -274,7 +268,7 @@ class VFBCircuitBrowser extends Component { let errorMessage = "Not enough input queries to create a graph, needs 2."; if ( this.state.neurons?.[0].id != "" && this.state.neurons?.[1].id != "" ){ - errorMessage = "Graph not available for " + this.state.neurons.join(","); + errorMessage = "Graph not available for " + this.state.neurons.map(a => `'${a.id}'`).join(","); } return ( this.state.loading From e7aea8676aadfeb54bf75a629f246ea21e998d91 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Mon, 1 Mar 2021 13:59:44 +0000 Subject: [PATCH 06/80] fix eye icon in list viewer --- components/interface/VFBListViewer/ListViewerControlsMenu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/interface/VFBListViewer/ListViewerControlsMenu.js b/components/interface/VFBListViewer/ListViewerControlsMenu.js index 27f5de698..623cb7ee5 100644 --- a/components/interface/VFBListViewer/ListViewerControlsMenu.js +++ b/components/interface/VFBListViewer/ListViewerControlsMenu.js @@ -210,9 +210,9 @@ class ListViewerControlsMenu extends Component { button.list = list; } if (self.props.instance.isVisible()) { - button.icon.props.className = "fa fa-eye"; - } else { button.icon.props.className = "fa fa-eye-slash"; + } else { + button.icon.props.className = "fa fa-eye"; } }); From cb53ec8ecaafec1ec84fe6cdc07d44590c916faa Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 12 Mar 2021 20:58:04 -0800 Subject: [PATCH 07/80] #657 Save work in progress for adding weights to circuit browser --- .../circuitBrowserConfiguration.js | 13 +-- .../interface/VFBCircuitBrowser/Controls.js | 53 +++++++---- .../VFBCircuitBrowser/QueryParser.js | 36 ++++--- .../VFBCircuitBrowser/VFBCircuitBrowser.js | 93 ++++++++++++++++--- 4 files changed, 148 insertions(+), 47 deletions(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index fdd2832e8..b687e1236 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -1,16 +1,16 @@ -var locationCypherQuery = ( instances, hops , weight ) => ({ - statements: [ +var locationCypherQuery = ( instances, hops, weight ) => ({ + "statements": [ { "statement" : "WITH [" + instances + "] AS neurons" + " WITH neurons[0] as root, neurons[1..] AS neurons" + " MATCH p=(x:Neuron {short_form: root})-[:synapsed_to*.." + hops.toString() + "]->(y:Neuron)" + " WHERE y.short_form IN neurons AND" + " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")" - + " WITH root, relationships(p) as fu" + + " WITH root, relationships(p) as fu, p AS pp" + " UNWIND fu as r" - + " WITH root, startNode(r) AS a, endNode(r) AS b" - + " MATCH p=(a)-[:synapsed_to]-(b)" - + " RETURN p, root", + + " WITH root, startNode(r) AS a, endNode(r) AS b, pp" + + " MATCH p=(a)<-[:synapsed_to]-(b)" + + " RETURN pp, p, root", "resultDataContents": ["graph"] } ] @@ -26,6 +26,7 @@ var configuration = { }, "link" : { "label" : "label", + "weight" : "weight", "visible" : true, "tooltip" : "label" } diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index 90977918e..54fa76eed 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -12,6 +12,7 @@ import PropTypes from 'prop-types'; import Paper from '@material-ui/core/Paper'; import Grid from '@material-ui/core/Grid'; import TextField from '@material-ui/core/TextField'; +import Input from '@material-ui/core/Input'; import Slider from '@material-ui/core/Slider'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; import ImportExportIcon from '@material-ui/icons/ImportExport'; @@ -137,6 +138,7 @@ class Controls extends Component { this.neuronTextfieldModified = this.neuronTextfieldModified.bind(this); this.typingTimeout = this.typingTimeout.bind(this); this.sliderChange = this.sliderChange.bind(this); + this.weightChange = this.weightChange.bind(this); this.fieldsValidated = this.fieldsValidated.bind(this); this.deleteNeuronField = this.deleteNeuronField.bind(this); this.getUpdatedNeuronFields = this.getUpdatedNeuronFields.bind(this); @@ -276,6 +278,15 @@ class Controls extends Component { this.props.updateHops(value); } } + + weightChange (event ) { + if (event.key === 'Enter') { + // Request new queries results with updated hops only if textfields contain valid neuron IDs + if ( this.fieldsValidated(this.state.neuronFields) ) { + this.props.updateWeight(event.target.value); + } + } + } /** * Update neuron fields if there's a query preselected. @@ -388,7 +399,7 @@ class Controls extends Component { key={field.id} onChange={this.neuronTextfieldModified} inputProps={{ ...params.inputProps, style: { color: "white" , paddingLeft : "10px" } }} - InputLabelProps={{ ...params.inputProps,style: { color: "white" } }} + InputLabelProps={{ ...params.inputProps,style: { color: "white", paddingLeft : "10px" } }} /> )} /> @@ -425,23 +436,33 @@ class Controls extends Component { - - - Hops + + + + Hops + + + + - - + + + Weight + + + + - + diff --git a/components/interface/VFBCircuitBrowser/QueryParser.js b/components/interface/VFBCircuitBrowser/QueryParser.js index f405b78eb..3dca7058d 100644 --- a/components/interface/VFBCircuitBrowser/QueryParser.js +++ b/components/interface/VFBCircuitBrowser/QueryParser.js @@ -28,12 +28,14 @@ export function queryParser (e) { // Only keep track of new links, avoid duplicates if ( newLink ) { - linksMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label] }); + linksMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); } }); }); + console.log("LinksMap ", linksMap); + // Loop through nodes from query and create nodes for graph data.forEach(({ graph }) => { graph.nodes.forEach(({ id, labels, properties }) => { @@ -84,25 +86,31 @@ export function queryParser (e) { let targetNode = nodesMap.get(n[i].target); if (targetNode !== undefined) { - // Create new link for graph - let link = { source: sourceNode, name : n[i].label, target: targetNode, targetNode: targetNode }; - links.push( link ); + let match = links.find( link => link.target === targetNode && link.source === sourceNode); + let reverse = links.find( link => link.target === sourceNode && link.source === targetNode); + if ( !match ) { + // Create new link for graph + let link = { source: sourceNode, label : n[i].weight, weight : n[i].weight, target: targetNode, targetNode: targetNode, curvature: .5 }; + links.push( link ); - // Assign neighbors to nodes and links - !sourceNode.neighbors && (sourceNode.neighbors = []); - !targetNode.neighbors && (targetNode.neighbors = []); - sourceNode.neighbors.push(targetNode); - targetNode.neighbors.push(sourceNode); + // Assign neighbors to nodes and links + !sourceNode.neighbors && (sourceNode.neighbors = []); + !targetNode.neighbors && (targetNode.neighbors = []); + sourceNode.neighbors.push(targetNode); + targetNode.neighbors.push(sourceNode); - // Assign links to nodes - !sourceNode.links && (sourceNode.links = []); - !targetNode.links && (targetNode.links = []); - sourceNode.links.push(link); - targetNode.links.push(link); + // Assign links to nodes + !sourceNode.links && (sourceNode.links = []); + !targetNode.links && (targetNode.links = []); + sourceNode.links.push(link); + targetNode.links.push(link); + } } } } }); + + console.log("Links ", links); // Worker is done, notify main thread this.postMessage({ resultMessage: "OK", params: { results: { nodes, links }, colorLabels : presentColorLabels } }); diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 23b49b99d..053a448d3 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -58,6 +58,7 @@ class VFBCircuitBrowser extends Component { dropDownAnchorEl : null, neurons : [{ id : "", label : "" } , { id : "", label : "" }], hops : Math.ceil((configuration.maxHops - configuration.minHops) / 2), + weight : 70, reload : false } this.updateGraph = this.updateGraph.bind(this); @@ -68,6 +69,7 @@ class VFBCircuitBrowser extends Component { this.zoomOut = this.zoomOut.bind(this); this.queriesUpdated = this.queriesUpdated.bind(this); this.updateHops = this.updateHops.bind(this); + this.updateWeight = this.updateWeight.bind(this); this.resize = this.resize.bind(this); this.highlightNodes = new Set(); @@ -86,7 +88,7 @@ class VFBCircuitBrowser extends Component { componentDidMount () { let self = this; this.__isMounted = true; - this.updateGraph(this.state.neurons , Math.ceil((configuration.maxHops - configuration.minHops) / 2)); + this.updateGraph(this.state.neurons , Math.ceil((configuration.maxHops - configuration.minHops) / 2), this.state.weight); const { circuitQuerySelected } = this.props; this.circuitQuerySelected = circuitQuerySelected; } @@ -124,7 +126,7 @@ class VFBCircuitBrowser extends Component { // Request graph update if the list of new neurons is not the same if ( !this.state.loading && !matched ) { - this.updateGraph(neurons, this.state.hops); + this.updateGraph(neurons, this.state.hops, this.state.weight); } } @@ -133,7 +135,12 @@ class VFBCircuitBrowser extends Component { */ updateHops (hops) { this.setState({ hops : hops }); - this.updateGraph(this.state.neurons, hops); + this.updateGraph(this.state.neurons, hops, this.state.weight); + } + + updateWeight (weight) { + this.setState({ weight : weight }); + this.updateGraph(this.state.neurons, this.state.hops, weight); } resetCamera () { @@ -177,12 +184,12 @@ class VFBCircuitBrowser extends Component { /** * Re-render graph with a new instance */ - updateGraph (neurons, hops) { + updateGraph (neurons, hops, weight) { if (this.__isMounted){ // Show loading spinner while cypher query search occurs - this.setState({ loading : true , neurons : neurons, hops : hops, queryLoaded : false }); + this.setState({ loading : true , neurons : neurons, hops : hops, weight : weight, queryLoaded : false }); // Perform cypher query. TODO: Remove hardcoded weight once edge weight is implemented - this.queryResults(cypherQuery(neurons.map(a => `'${a.id}'`).join(","), hops, 70)); + this.queryResults(cypherQuery(neurons.map(a => `'${a.id}'`).join(","), hops, weight)); } } @@ -280,9 +287,11 @@ class VFBCircuitBrowser extends Component { this.state.graph.nodes.length > 0 } resetCamera={self.resetCamera} zoomIn={self.zoomIn} @@ -298,12 +307,69 @@ class VFBCircuitBrowser extends Component { data={this.state.graph} // Create the Graph as 2 Dimensional d2={true} - // Node label, used in tooltip when hovering over Node nodeLabel={node => node.path} + // Relationship label, placed in Link + linkLabel={link => link.label} + // Node label, used in tooltip when hovering over Node + linkCanvasObjectMode={() => "after"} + linkCanvasObject={(link, ctx) => { + const MAX_FONT_SIZE = 5; + const LABEL_NODE_MARGIN = 1 * 1.5; + + const start = link.source; + const end = link.target; + + // ignore unbound links + if (typeof start !== 'object' || typeof end !== 'object') { + return; + } + + // calculate label positioning + const textPos = Object.assign({},...['x', 'y'].map(c => ({ [c]: start[c] + (end[c] - start[c]) / 2 }))); + + const relLink = { x: end.x - start.x, y: end.y - start.y }; + + const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2; + + let textAngle = Math.atan2(relLink.y, relLink.x); + // maintain label vertical orientation for legibility + if (textAngle > Math.PI / 2) { + textAngle = -(Math.PI - textAngle); + } + if (textAngle < -Math.PI / 2) { + textAngle = -(-Math.PI - textAngle); + } + + const label = link.label; + + // estimate fontSize to fit in link length + ctx.font = '1px Sans-Serif'; + const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width); + ctx.font = `${fontSize}px Sans-Serif`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding + + const calculatedYPos = Math.abs(end.y - start.y); + + const xPos = link?.__controlPoints ? link.__controlPoints[0] : textPos.x; + const yPos = link?.__controlPoints ? link?.__controlPoints[1] / 2 : textPos.y ; + + // draw text label (with background rect) + ctx.save(); + ctx.translate(textPos.x, yPos - 5); + ctx.rotate(textAngle); + + + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = 'white'; + + const curvatureSize = link?.curvature ? link.curvature : 1; + ctx.fillText(label, 0, 0); + ctx.restore(); + }} nodeRelSize={20} nodeSize={30} - // Relationship label, placed in Link - linkLabel={link => link.name} // Assign background color to Canvas backgroundColor = {stylingConfiguration.canvasColor} // Assign color to Links connecting Nodes @@ -351,7 +417,7 @@ class VFBCircuitBrowser extends Component { // bu = Bottom Up, creates Graph with root at bottom dagMode="lr" dagLevelDistance = {100} - onDagError={loopNodeIds => {}} + onDagError={loopNodeIds => console.log("Node IDS " , loopNodeIds)} // Handles clicking event on an individual node onNodeClick = { (node,event) => this.handleNodeLeftClick(node,event) } ref={this.graphRef} @@ -360,14 +426,19 @@ class VFBCircuitBrowser extends Component { // Allow camera pan and zoom with mouse enableZoomPanInteraction={true} // Width of links - linkWidth={1.25} + linkWidth={link => link.weight ? Math.log(link.weight) : 1 } + linkCurvature='curvature' + linkDirectionalArrowLength={link => Math.log(link.weight) * 3 } + linkDirectionalArrowRelPos={.5 } controls = { this.state.graph.nodes.length > 0 } resetCamera={self.resetCamera} zoomIn={self.zoomIn} From 9dacf4476f58de2d816874a3ecf17139c8597294 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 15 Mar 2021 16:11:16 -0700 Subject: [PATCH 08/80] #657 Fixing relationships --- .../circuitBrowserConfiguration.js | 6 +-- .../VFBCircuitBrowser/QueryParser.js | 51 ++++++++++--------- .../VFBCircuitBrowser/VFBCircuitBrowser.js | 8 +-- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index b687e1236..498bb35d3 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -8,10 +8,10 @@ var locationCypherQuery = ( instances, hops, weight ) => ({ + " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")" + " WITH root, relationships(p) as fu, p AS pp" + " UNWIND fu as r" - + " WITH root, startNode(r) AS a, endNode(r) AS b, pp" + + " WITH root, startNode(r) AS a, endNode(r) AS b, pp, id(r) as id" + " MATCH p=(a)<-[:synapsed_to]-(b)" - + " RETURN pp, p, root", - "resultDataContents": ["graph"] + + " RETURN root, collect(distinct pp) as pp, collect(distinct p) as p, collect(distinct id) as fr", + "resultDataContents": ["row", "graph"] } ] }); diff --git a/components/interface/VFBCircuitBrowser/QueryParser.js b/components/interface/VFBCircuitBrowser/QueryParser.js index 3dca7058d..0736d07ba 100644 --- a/components/interface/VFBCircuitBrowser/QueryParser.js +++ b/components/interface/VFBCircuitBrowser/QueryParser.js @@ -7,35 +7,40 @@ export function queryParser (e) { let data = graphData.results[0].data; let nodes = [], links = []; let linksMap = new Map(); + let reverseMap = new Map(); let nodesMap = new Map(); let presentColorLabels = new Array(); // Creates links map from Relationships, avoid duplicates - data.forEach(({ graph }) => { - graph.relationships.forEach(({ startNode, endNode, properties }) => { - if (linksMap.get(startNode) === undefined) { - linksMap.set(startNode, new Array()); - } - - let newLink = true; - linksMap.get(startNode).find( function ( ele ) { - if ( ele.target !== endNode ) { - newLink = true; - } else { - newLink = false; + data.forEach(({ graph, row }) => { + graph.relationships.forEach(({ startNode, endNode, properties, id }) => { + if ( row[3].includes(parseInt(id)) ) { + if (linksMap.get(startNode) === undefined) { + linksMap.set(startNode, new Array()); } - }); + + let newLink = true; + linksMap.get(startNode).find( function ( ele ) { + if ( ele.target !== endNode ) { + newLink = true; + } else { + newLink = false; + } + }); - // Only keep track of new links, avoid duplicates - if ( newLink ) { - linksMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); + // Only keep track of new links, avoid duplicates + if ( newLink ) { + linksMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); + } + } else { + if (reverseMap.get(startNode) === undefined) { + reverseMap.set(startNode, new Array()); + } + reverseMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); } - }); }); - console.log("LinksMap ", linksMap); - // Loop through nodes from query and create nodes for graph data.forEach(({ graph }) => { graph.nodes.forEach(({ id, labels, properties }) => { @@ -84,13 +89,13 @@ export function queryParser (e) { if (n !== undefined){ for (var i = 0 ; i < n.length; i++){ let targetNode = nodesMap.get(n[i].target); - if (targetNode !== undefined) { let match = links.find( link => link.target === targetNode && link.source === sourceNode); - let reverse = links.find( link => link.target === sourceNode && link.source === targetNode); + let reverse = reverseMap.get(targetNode.id.toString())?.find( node => node.target === sourceNode.id.toString()); if ( !match ) { + const label = reverse ? n[i].weight + "[" + reverse.weight + "]" : n[i].weight; // Create new link for graph - let link = { source: sourceNode, label : n[i].weight, weight : n[i].weight, target: targetNode, targetNode: targetNode, curvature: .5 }; + let link = { source: sourceNode, label : label, weight : n[i].weight, target: targetNode, targetNode: targetNode, curvature: .5 }; links.push( link ); // Assign neighbors to nodes and links @@ -110,8 +115,6 @@ export function queryParser (e) { } }); - console.log("Links ", links); - // Worker is done, notify main thread this.postMessage({ resultMessage: "OK", params: { results: { nodes, links }, colorLabels : presentColorLabels } }); } \ No newline at end of file diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 053a448d3..2cb2256c7 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -356,7 +356,7 @@ class VFBCircuitBrowser extends Component { // draw text label (with background rect) ctx.save(); - ctx.translate(textPos.x, yPos - 5); + ctx.translate(textPos.x, yPos - 10); ctx.rotate(textAngle); @@ -417,7 +417,7 @@ class VFBCircuitBrowser extends Component { // bu = Bottom Up, creates Graph with root at bottom dagMode="lr" dagLevelDistance = {100} - onDagError={loopNodeIds => console.log("Node IDS " , loopNodeIds)} + onDagError={loopNodeIds => {}} // Handles clicking event on an individual node onNodeClick = { (node,event) => this.handleNodeLeftClick(node,event) } ref={this.graphRef} @@ -428,8 +428,8 @@ class VFBCircuitBrowser extends Component { // Width of links linkWidth={link => link.weight ? Math.log(link.weight) : 1 } linkCurvature='curvature' - linkDirectionalArrowLength={link => Math.log(link.weight) * 3 } - linkDirectionalArrowRelPos={.5 } + linkDirectionalArrowLength={link => link.weight ? Math.log(link.weight) * 3 : .5} + linkDirectionalArrowRelPos={.25} controls = { Date: Thu, 18 Mar 2021 16:12:19 -0700 Subject: [PATCH 09/80] #657 Finish adding edges to circuit browser --- .../circuitBrowserConfiguration.js | 2 + .../interface/VFBCircuitBrowser/Controls.js | 38 ++---- .../VFBCircuitBrowser/QueryParser.js | 8 +- .../VFBCircuitBrowser/VFBCircuitBrowser.js | 129 ++++++++++++++---- 4 files changed, 128 insertions(+), 49 deletions(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 498bb35d3..b91f86870 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -6,12 +6,14 @@ var locationCypherQuery = ( instances, hops, weight ) => ({ + " MATCH p=(x:Neuron {short_form: root})-[:synapsed_to*.." + hops.toString() + "]->(y:Neuron)" + " WHERE y.short_form IN neurons AND" + " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")" + + " AND none(rel in relationships(p) WHERE endNode(rel) = x OR startNode(rel) = y)" + " WITH root, relationships(p) as fu, p AS pp" + " UNWIND fu as r" + " WITH root, startNode(r) AS a, endNode(r) AS b, pp, id(r) as id" + " MATCH p=(a)<-[:synapsed_to]-(b)" + " RETURN root, collect(distinct pp) as pp, collect(distinct p) as p, collect(distinct id) as fr", "resultDataContents": ["row", "graph"] + } ] }); diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index 54fa76eed..8b0b1c1d9 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -134,6 +134,8 @@ class Controls extends Component { neuronFields : [{ id : "", label : "" } , { id : "", label : "" }], filteredResults : {} }; + this.weight = this.props.weight; + this.hops = this.props.hops; this.addNeuron = this.addNeuron.bind(this); this.neuronTextfieldModified = this.neuronTextfieldModified.bind(this); this.typingTimeout = this.typingTimeout.bind(this); @@ -173,11 +175,6 @@ class Controls extends Component { // Update state with one fewer neuron textfield this.setState( { neuronFields : neurons } ); - - // If neuron fields are validated, let the VFBCircuitBrowser component know, it will do a graph update - if ( this.fieldsValidated(neurons) ) { - this.props.queriesUpdated(neurons); - } } /** @@ -265,7 +262,6 @@ class Controls extends Component { // If text fields contain valid ids, perform query if ( this.fieldsValidated(neurons) ) { this.setState( { neuronFields : neurons } ); - this.props.queriesUpdated(neurons); } } @@ -273,19 +269,11 @@ class Controls extends Component { * Hops slider has been dragged, value has changed */ sliderChange (event, value ) { - // Request new queries results with updated hops only if textfields contain valid neuron IDs - if ( this.fieldsValidated(this.state.neuronFields) ) { - this.props.updateHops(value); - } + this.hops = value; } weightChange (event ) { - if (event.key === 'Enter') { - // Request new queries results with updated hops only if textfields contain valid neuron IDs - if ( this.fieldsValidated(this.state.neuronFields) ) { - this.props.updateWeight(event.target.value); - } - } + this.weight = event.target.value; } /** @@ -316,10 +304,6 @@ class Controls extends Component { } } - if ( this.fieldsValidated(neuronFields) ) { - this.props.queriesUpdated(neuronFields); - } - return neuronFields; } @@ -444,7 +428,7 @@ class Controls extends Component { Weight - - + + + + + diff --git a/components/interface/VFBCircuitBrowser/QueryParser.js b/components/interface/VFBCircuitBrowser/QueryParser.js index 0736d07ba..27c5fb74b 100644 --- a/components/interface/VFBCircuitBrowser/QueryParser.js +++ b/components/interface/VFBCircuitBrowser/QueryParser.js @@ -33,6 +33,7 @@ export function queryParser (e) { linksMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); } } else { + // Keep track of reverse links if (reverseMap.get(startNode) === undefined) { reverseMap.set(startNode, new Array()); } @@ -93,9 +94,12 @@ export function queryParser (e) { let match = links.find( link => link.target === targetNode && link.source === sourceNode); let reverse = reverseMap.get(targetNode.id.toString())?.find( node => node.target === sourceNode.id.toString()); if ( !match ) { - const label = reverse ? n[i].weight + "[" + reverse.weight + "]" : n[i].weight; + // Create tooltip label for link and weight + const tooltip = "Label : " + n[i].label + '
' + + "Weight : " + (reverse ? n[i].weight + " [" + reverse.weight + "]" : n[i].weight); + const weightLabel = reverse ? n[i].weight + " [" + reverse.weight + "]" : n[i].weight; // Create new link for graph - let link = { source: sourceNode, label : label, weight : n[i].weight, target: targetNode, targetNode: targetNode, curvature: .5 }; + let link = { source: sourceNode, label : tooltip, weightLabel : weightLabel, weight : n[i].weight, target: targetNode, targetNode: targetNode, curvature: .75 }; links.push( link ); // Assign neighbors to nodes and links diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 2cb2256c7..4bed42d03 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -135,12 +135,10 @@ class VFBCircuitBrowser extends Component { */ updateHops (hops) { this.setState({ hops : hops }); - this.updateGraph(this.state.neurons, hops, this.state.weight); } updateWeight (weight) { this.setState({ weight : weight }); - this.updateGraph(this.state.neurons, this.state.hops, weight); } resetCamera () { @@ -187,9 +185,9 @@ class VFBCircuitBrowser extends Component { updateGraph (neurons, hops, weight) { if (this.__isMounted){ // Show loading spinner while cypher query search occurs - this.setState({ loading : true , neurons : neurons, hops : hops, weight : weight, queryLoaded : false }); + this.setState({ loading : true , neurons : neurons ? neurons : this.state.neurons, hops : hops ? hops : this.state.hops, weight : weight ? weight : this.state.weight, queryLoaded : false }); // Perform cypher query. TODO: Remove hardcoded weight once edge weight is implemented - this.queryResults(cypherQuery(neurons.map(a => `'${a.id}'`).join(","), hops, weight)); + this.queryResults(cypherQuery(neurons ? neurons.map(a => `'${a.id}'`).join(",") : this.state.neurons, hops ? hops : this.state.hops, weight ? weight : this.state.weight)); } } @@ -265,6 +263,14 @@ class VFBCircuitBrowser extends Component { } context.fillText(line, x, y); } + + // Calculate link middle point + getQuadraticXY (t, sx, sy, cp1x, cp1y, ex, ey) { + return { + x: (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cp1x + t * t * ex, + y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey, + }; + } render () { let self = this; @@ -285,7 +291,7 @@ class VFBCircuitBrowser extends Component { ?

{errorMessage}

node.path} // Relationship label, placed in Link linkLabel={link => link.label} - // Node label, used in tooltip when hovering over Node - linkCanvasObjectMode={() => "after"} + // Width of links, log(weight) + linkWidth={link => link.weight ? Math.log(link.weight) : 1 } + linkCurvature='curvature' + linkDirectionalArrowLength={link => link.weight ? Math.log(link.weight) * 3 : .5} + linkDirectionalArrowRelPos={.75} linkCanvasObject={(link, ctx) => { const MAX_FONT_SIZE = 5; const LABEL_NODE_MARGIN = 1 * 1.5; @@ -323,10 +332,23 @@ class VFBCircuitBrowser extends Component { if (typeof start !== 'object' || typeof end !== 'object') { return; } - + // calculate label positioning - const textPos = Object.assign({},...['x', 'y'].map(c => ({ [c]: start[c] + (end[c] - start[c]) / 2 }))); + let textPos = Object.assign({},...['x', 'y'].map(c => ({ [c]: start[c] + (end[c] - start[c]) / 2 }))); + + if (link?.curvature && link?.__controlPoints ) { + // Get mid point of link, save as position of weight label text + textPos = this.getQuadraticXY( + .5, + start.x, + start.y, + link?.__controlPoints[0], + link?.__controlPoints[1], + end.x, + end.y + ); + } const relLink = { x: end.x - start.x, y: end.y - start.y }; const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2; @@ -340,7 +362,7 @@ class VFBCircuitBrowser extends Component { textAngle = -(-Math.PI - textAngle); } - const label = link.label; + const label = link.weightLabel; // estimate fontSize to fit in link length ctx.font = '1px Sans-Serif'; @@ -349,22 +371,86 @@ class VFBCircuitBrowser extends Component { const textWidth = ctx.measureText(label).width; const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding - const calculatedYPos = Math.abs(end.y - start.y); - - const xPos = link?.__controlPoints ? link.__controlPoints[0] : textPos.x; - const yPos = link?.__controlPoints ? link?.__controlPoints[1] / 2 : textPos.y ; - // draw text label (with background rect) ctx.save(); - ctx.translate(textPos.x, yPos - 10); + ctx.translate(textPos.x,textPos.y); ctx.rotate(textAngle); + // draw black rectangle + ctx.fillStyle = 'rgba(0, 0, 0, 1)'; + ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, ...bckgDimensions); + // draw weight label text + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = stylingConfiguration.defaultLinkColor; + ctx.setLineDash([5, 5]); + ctx.fillText(label, 0, 0); + ctx.restore(); + }} + // Node label, used in tooltip when hovering over Node + linkCanvasObjectMode={() => "after"} + linkCanvasObject={(link, ctx) => { + const MAX_FONT_SIZE = 5; + const LABEL_NODE_MARGIN = 1 * 1.5; + + const start = link.source; + const end = link.target; + + if ( start.y < end.y && !link.modified ) { + link.curvature = -1 * link.curvature; + link.modified = true; + } + // ignore unbound links + if (typeof start !== 'object' || typeof end !== 'object') { + return; + } + // calculate label positioning + let textPos = Object.assign({},...['x', 'y'].map(c => ({ [c]: start[c] + (end[c] - start[c]) / 2 }))); + + if (link?.__controlPoints ) { + textPos = this.getQuadraticXY( + .5, + start.x, + start.y, + link?.__controlPoints[0], + link?.__controlPoints[1], + end.x, + end.y + ); + } + const relLink = { x: end.x - start.x, y: end.y - start.y }; + + const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2; + + let textAngle = Math.atan2(relLink.y, relLink.x); + // maintain label vertical orientation for legibility + if (textAngle > Math.PI / 2) { + textAngle = -(Math.PI - textAngle); + } + if (textAngle < -Math.PI / 2) { + textAngle = -(-Math.PI - textAngle); + } + + const label = link.weightLabel; + + // estimate fontSize to fit in link length + ctx.font = '1px Sans-Serif'; + const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width); + ctx.font = `${fontSize}px Sans-Serif`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding + + // draw text label (with background rect) + ctx.save(); + ctx.translate(textPos.x,textPos.y); + ctx.rotate(textAngle); + ctx.fillStyle = 'rgba(0, 0, 0, 1)'; + ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, ...bckgDimensions); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'white'; - - const curvatureSize = link?.curvature ? link.curvature : 1; + ctx.setLineDash([5, 5]); ctx.fillText(label, 0, 0); ctx.restore(); }} @@ -425,14 +511,9 @@ class VFBCircuitBrowser extends Component { enableNodeDrag={false} // Allow camera pan and zoom with mouse enableZoomPanInteraction={true} - // Width of links - linkWidth={link => link.weight ? Math.log(link.weight) : 1 } - linkCurvature='curvature' - linkDirectionalArrowLength={link => link.weight ? Math.log(link.weight) * 3 : .5} - linkDirectionalArrowRelPos={.25} controls = { Date: Sat, 20 Mar 2021 12:03:24 +0000 Subject: [PATCH 10/80] ensuring all links are returned --- components/configuration/VFBTree/VFBTreeConfiguration.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/configuration/VFBTree/VFBTreeConfiguration.js b/components/configuration/VFBTree/VFBTreeConfiguration.js index 9756a0fc2..917dc4234 100644 --- a/components/configuration/VFBTree/VFBTreeConfiguration.js +++ b/components/configuration/VFBTree/VFBTreeConfiguration.js @@ -10,8 +10,10 @@ var treeCypherQuery = instance => ({ + "<-[:depicts]-(tc:Template)<-[ie:in_register_with]-(c:Individual)-[:depicts]->(image:" + "Individual)-[r:INSTANCEOF]->(anat:Class:Nervous_system) WHERE exists(ie.index) WITH root, anat,r,image" + " MATCH p=allshortestpaths((root)<-[:SUBCLASSOF|part_of*..]-(anat)) " + + "UNWIND nodes(p) as n UNWIND nodes(p) as m WITH * WHERE id(n) < id(m) " + + "MATCH path = allShortestPaths( (n)-[:SUBCLASSOF|part_of*..1]-(m) ) " + "RETURN collect(distinct { node_id: id(anat), short_form: anat.short_form, image: image.short_form })" - + " AS image_nodes, id(root) AS root, collect(p)", + + " AS image_nodes, id(root) AS root, collect(path)", "resultDataContents": ["row", "graph"] } ] From e64f8ad0f8ce934dad1c73743ed33071160c3b54 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Sat, 20 Mar 2021 16:14:40 +0000 Subject: [PATCH 11/80] handling both has_license and license edges --- model/vfb.xmi | 14 +++++++------- tests/jest/vfb/batch1/term-info-tests.js | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/model/vfb.xmi b/model/vfb.xmi index 8e12ceafa..2b95dbe72 100644 --- a/model/vfb.xmi +++ b/model/vfb.xmi @@ -410,7 +410,7 @@ name="template_2_datasets_query" description="Get JSON for template_2_datasets query" returnType="//@libraries.3/@types.24" - query=""statement": "MATCH (t:Template) <-[depicts]-(tc:Template)-[:in_register_with]-(c:Individual)-[:depicts]->(ai:Individual)-[:has_source]->(ds:DataSet) WHERE t.short_form in [{ID}] WITH distinct ds, t CALL apoc.cypher.run('WITH ds, template_anat OPTIONAL MATCH (ds) <- [:has_source]-(i:Individual) <-[:depicts]- (channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat) RETURN template, channel, template_anat, i, irw limit 5', {ds:ds, template_anat:t}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, ds OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,ds OPTIONAL MATCH (ds)-[rp:has_reference]->(p:pub) WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS pubs,ds,anatomy_channel_image OPTIONAL MATCH (ds)-[:has_license]->(l:License) WITH collect ({ icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }) as license,ds,anatomy_channel_image,pubs OPTIONAL MATCH (ds) <-[:has_source]-(i:Individual) WITH i, ds, anatomy_channel_image, pubs, license OPTIONAL MATCH (i)-[:INSTANCEOF]-(c:Class) WITH DISTINCT { images: count(distinct i),types: count(distinct c) } as dataset_counts,ds,anatomy_channel_image,pubs,license RETURN { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} as dataset, 'bac066c' AS version, 'template_2_datasets_query' AS query, anatomy_channel_image, pubs, license, dataset_counts", "parameters" : { "ID" : "$ID" }" + query=""statement": "MATCH (t:Template) <-[depicts]-(tc:Template)-[:in_register_with]-(c:Individual)-[:depicts]->(ai:Individual)-[:has_source]->(ds:DataSet) WHERE t.short_form in [{ID}] WITH distinct ds, t CALL apoc.cypher.run('WITH ds, template_anat OPTIONAL MATCH (ds) <- [:has_source]-(i:Individual) <-[:depicts]- (channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat) RETURN template, channel, template_anat, i, irw limit 5', {ds:ds, template_anat:t}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, ds OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,ds OPTIONAL MATCH (ds)-[rp:has_reference]->(p:pub) WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS pubs,ds,anatomy_channel_image OPTIONAL MATCH (ds)-[:has_license|license]->(l:License) WITH collect ({ icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }) as license,ds,anatomy_channel_image,pubs OPTIONAL MATCH (ds) <-[:has_source]-(i:Individual) WITH i, ds, anatomy_channel_image, pubs, license OPTIONAL MATCH (i)-[:INSTANCEOF]-(c:Class) WITH DISTINCT { images: count(distinct i),types: count(distinct c) } as dataset_counts,ds,anatomy_channel_image,pubs,license RETURN { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} as dataset, 'bac066c' AS version, 'template_2_datasets_query' AS query, anatomy_channel_image, pubs, license, dataset_counts", "parameters" : { "ID" : "$ID" }" countQuery=""statement": "MATCH (t:Template)<-[depicts]-(tc:Template)-[:in_register_with]-(c:Individual)-[:depicts]->(ai:Individual)-[:has_source]->(ds:DataSet) WHERE t.short_form in [{ID}] WITH distinct ds RETURN count(ds) as count", "parameters" : { "ID" : "$ID" }"/> @@ -604,7 +604,7 @@ name="Get JSON for Cluster" description="Get JSON for Cluster" runForCount="false" - query=""statement": "MATCH (primary:Cluster) WHERE primary.short_form in [{ID}] WITH primary OPTIONAL MATCH (primary)-[:has_source]->(ds:DataSet)-[:has_license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link, ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds) } }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l) } }}) AS dataset_license,primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } ) END AS parents ,primary,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.uri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } }) END AS relationships ,primary,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: s.link_base[0], accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s) } , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:hasDbXref]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: s.link_base[0], accession: coalesce(dbx.accession, ''), link_text: primary.label + ' on ' + s.label, site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s) } , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,dataset_license,parents,relationships OPTIONAL MATCH (primary)<-[:depicts]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) WITH template, channel, template_anat, irw, primary, dataset_license, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE collect ({ channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel) } , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique) } ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template) } , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat) } ,image_folder: irw.folder[0], index: coalesce(irw.index[0], []) + [] }}) END AS channel_image,primary,dataset_license,parents,relationships,xrefs OPTIONAL MATCH (o:Individual)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.uri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } }) END AS related_individuals ,primary,dataset_license,parents,relationships,xrefs,channel_image RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary) } , description : coalesce(primary.description, []), comment : coalesce(primary.`annotation-comment`, []) } AS term, 'Get JSON for Individual:Anatomy' AS query, 'ca9ab19' AS version , dataset_license, parents, relationships, xrefs, channel_image, related_individuals", "parameters" : { "ID" : "$ID" }" + query=""statement": "MATCH (primary:Cluster) WHERE primary.short_form in [{ID}] WITH primary OPTIONAL MATCH (primary)-[:has_source]->(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link, ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds) } }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l) } }}) AS dataset_license,primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } ) END AS parents ,primary,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.uri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } }) END AS relationships ,primary,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: s.link_base[0], accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s) } , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:hasDbXref]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: s.link_base[0], accession: coalesce(dbx.accession, ''), link_text: primary.label + ' on ' + s.label, site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s) } , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,dataset_license,parents,relationships OPTIONAL MATCH (primary)<-[:depicts]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) WITH template, channel, template_anat, irw, primary, dataset_license, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE collect ({ channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel) } , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique) } ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template) } , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat) } ,image_folder: irw.folder[0], index: coalesce(irw.index[0], []) + [] }}) END AS channel_image,primary,dataset_license,parents,relationships,xrefs OPTIONAL MATCH (o:Individual)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.uri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } }) END AS related_individuals ,primary,dataset_license,parents,relationships,xrefs,channel_image RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary) } , description : coalesce(primary.description, []), comment : coalesce(primary.`annotation-comment`, []) } AS term, 'Get JSON for Individual:Anatomy' AS query, 'ca9ab19' AS version , dataset_license, parents, relationships, xrefs, channel_image, related_individuals", "parameters" : { "ID" : "$ID" }" countQuery=""statement": "MATCH (primary:Cluster {short_form: {ID}} ) RETURN count(primary) as count", "parameters" : { "ID" : "$ID" }"> @@ -614,7 +614,7 @@ name="Get JSON for Template" description="Get JSON for Template" runForCount="false" - query=""statement": "MATCH (primary:Template) WHERE primary.short_form in [{ID}] WITH primary MATCH (channel:Individual)<-[irw:in_register_with]-(channel:Individual)-[:depicts]->(primary) WITH { index: coalesce(apoc.convert.toInteger(irw.index), []) + [], extent: irw.extent[0], center: irw.center[0], voxel: irw.voxel[0], orientation: coalesce(irw.orientation[0], ''), image_folder: coalesce(irw.folder[0],''), channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} } as template_channel,primary OPTIONAL MATCH (technique:Class)<-[:is_specified_output_of]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(primary) WHERE technique.short_form = 'FBbi_00000224' AND exists(irw.index) WITH primary, template_channel, collect ({ channel: channel, irw: irw}) AS painted_domains UNWIND painted_domains AS pd OPTIONAL MATCH (channel:Individual { short_form: pd.channel.short_form})-[:depicts]-(ai:Individual)-[:INSTANCEOF]->(c:Class) WITH collect({ anatomical_type: { short_form: c.short_form, label: coalesce(c.label,''), iri: c.iri, types: labels(c), symbol: coalesce(c.symbol[0], '')} , anatomical_individual: { short_form: ai.short_form, label: coalesce(ai.label,''), iri: ai.iri, types: labels(ai), symbol: coalesce(ai.symbol[0], '')} , folder: pd.irw.folder[0], center: coalesce (pd.irw.center, []), index: [] + coalesce (pd.irw.index, []) }) AS template_domains,primary,template_channel OPTIONAL MATCH (primary)-[:has_source]-(ds:DataSet)-[:has_license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary,template_channel,template_domains OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary,template_channel,template_domains,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,template_channel,template_domains,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, template_channel, template_domains, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,template_channel,template_domains,dataset_license,parents,relationships OPTIONAL MATCH (o:Individual)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS related_individuals ,primary,template_channel,template_domains,dataset_license,parents,relationships,xrefs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Template' AS query, 'bac066c' AS version , template_channel, template_domains, dataset_license, parents, relationships, xrefs, related_individuals", "parameters" : { "ID" : "$ID" }" + query=""statement": "MATCH (primary:Template) WHERE primary.short_form in [{ID}] WITH primary MATCH (channel:Individual)<-[irw:in_register_with]-(channel:Individual)-[:depicts]->(primary) WITH { index: coalesce(apoc.convert.toInteger(irw.index), []) + [], extent: irw.extent[0], center: irw.center[0], voxel: irw.voxel[0], orientation: coalesce(irw.orientation[0], ''), image_folder: coalesce(irw.folder[0],''), channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} } as template_channel,primary OPTIONAL MATCH (technique:Class)<-[:is_specified_output_of]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(primary) WHERE technique.short_form = 'FBbi_00000224' AND exists(irw.index) WITH primary, template_channel, collect ({ channel: channel, irw: irw}) AS painted_domains UNWIND painted_domains AS pd OPTIONAL MATCH (channel:Individual { short_form: pd.channel.short_form})-[:depicts]-(ai:Individual)-[:INSTANCEOF]->(c:Class) WITH collect({ anatomical_type: { short_form: c.short_form, label: coalesce(c.label,''), iri: c.iri, types: labels(c), symbol: coalesce(c.symbol[0], '')} , anatomical_individual: { short_form: ai.short_form, label: coalesce(ai.label,''), iri: ai.iri, types: labels(ai), symbol: coalesce(ai.symbol[0], '')} , folder: pd.irw.folder[0], center: coalesce (pd.irw.center, []), index: [] + coalesce (pd.irw.index, []) }) AS template_domains,primary,template_channel OPTIONAL MATCH (primary)-[:has_source]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary,template_channel,template_domains OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary,template_channel,template_domains,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,template_channel,template_domains,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, template_channel, template_domains, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,template_channel,template_domains,dataset_license,parents,relationships OPTIONAL MATCH (o:Individual)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS related_individuals ,primary,template_channel,template_domains,dataset_license,parents,relationships,xrefs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Template' AS query, 'bac066c' AS version , template_channel, template_domains, dataset_license, parents, relationships, xrefs, related_individuals", "parameters" : { "ID" : "$ID" }" countQuery=""statement": "MATCH (primary:Template {short_form: {ID}} ) RETURN count(primary) as count", "parameters" : { "ID" : "$ID" }"> @@ -624,7 +624,7 @@ name="Get JSON for pub" description="Fetches JSON for pub." runForCount="false" - query=""statement": "MATCH (primary:Individual:pub) WHERE primary.short_form in [{ID}] WITH primary OPTIONAL MATCH (primary)-[:has_reference]-(ds:DataSet)-[:has_license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'bac066c' AS version , dataset_license, {title: coalesce(primary.title[0], '') ,PubMed: coalesce(primary.PMID[0], ''), FlyBase: coalesce(primary.FlyBase[0], ''), DOI: coalesce(primary.DOI[0], '') }AS pub_specific_content", "parameters" : { "ID" : "$ID" }" + query=""statement": "MATCH (primary:Individual:pub) WHERE primary.short_form in [{ID}] WITH primary OPTIONAL MATCH (primary)-[:has_reference]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'bac066c' AS version , dataset_license, {title: coalesce(primary.title[0], '') ,PubMed: coalesce(primary.PMID[0], ''), FlyBase: coalesce(primary.FlyBase[0], ''), DOI: coalesce(primary.DOI[0], '') }AS pub_specific_content", "parameters" : { "ID" : "$ID" }" countQuery=""statement": "MATCH (primary:pub:Individual {short_form: {ID}} ) RETURN count(primary) as count", "parameters" : { "ID" : "$ID" }"> @@ -634,7 +634,7 @@ name="Get JSON for DataSet" description="Get JSON for DataSet" runForCount="false" - query=""statement": "MATCH (primary:DataSet) WHERE primary.short_form in [{ID}] WITH primary CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, anatomy_channel_image OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,anatomy_channel_image OPTIONAL MATCH (primary)-[:has_license]->(l:License) WITH collect ({ icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }) as license,primary,anatomy_channel_image,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS pubs,primary,anatomy_channel_image,xrefs,license OPTIONAL MATCH (primary)<-[:has_source]-(i:Individual) WITH i, primary, anatomy_channel_image, xrefs, license, pubs OPTIONAL MATCH (i)-[:INSTANCEOF]-(c:Class) WITH DISTINCT { images: count(distinct i),types: count(distinct c) } as dataset_counts,primary,anatomy_channel_image,xrefs,license,pubs RETURN { link : coalesce(primary.dataset_link[0], ''), core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for DataSet' AS query, 'bac066c' AS version , anatomy_channel_image, xrefs, license, pubs, dataset_counts", "parameters" : { "ID" : "$ID" }" + query=""statement": "MATCH (primary:DataSet) WHERE primary.short_form in [{ID}] WITH primary CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, anatomy_channel_image OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,anatomy_channel_image OPTIONAL MATCH (primary)-[:has_license|license]->(l:License) WITH collect ({ icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }) as license,primary,anatomy_channel_image,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS pubs,primary,anatomy_channel_image,xrefs,license OPTIONAL MATCH (primary)<-[:has_source]-(i:Individual) WITH i, primary, anatomy_channel_image, xrefs, license, pubs OPTIONAL MATCH (i)-[:INSTANCEOF]-(c:Class) WITH DISTINCT { images: count(distinct i),types: count(distinct c) } as dataset_counts,primary,anatomy_channel_image,xrefs,license,pubs RETURN { link : coalesce(primary.dataset_link[0], ''), core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for DataSet' AS query, 'bac066c' AS version , anatomy_channel_image, xrefs, license, pubs, dataset_counts", "parameters" : { "ID" : "$ID" }" countQuery=""statement": "MATCH (primary:DataSet {short_form: {ID}} ) RETURN count(primary) as count", "parameters" : { "ID" : "$ID" }"> diff --git a/tests/jest/vfb/batch1/term-info-tests.js b/tests/jest/vfb/batch1/term-info-tests.js index 3a5ac6558..75351b306 100644 --- a/tests/jest/vfb/batch1/term-info-tests.js +++ b/tests/jest/vfb/batch1/term-info-tests.js @@ -98,7 +98,7 @@ describe('VFB Term Info Component Tests', () => { if ( tabs[i].innerText === "Term Info" ) { tabs[i].click(); } - } + } }); await wait4selector(page, 'div#vfbterminfowidget', { visible: true, timeout : 500000}); }) @@ -125,7 +125,7 @@ describe('VFB Term Info Component Tests', () => { if ( tabs[i].innerText === "Term Info" ) { tabs[i].click(); } - } + } }); // Check term info component is visible again' await wait4selector(page, 'div#vfbterminfowidget', { visible: true, timeout : 500000}); From e0eee8603c142af08ded13885395b4356c240ccb Mon Sep 17 00:00:00 2001 From: Rob Court Date: Thu, 25 Mar 2021 09:35:44 +0000 Subject: [PATCH 12/80] limiting to 25 paths --- .../VFBCircuitBrowser/circuitBrowserConfiguration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index b91f86870..3f96544c5 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -7,7 +7,7 @@ var locationCypherQuery = ( instances, hops, weight ) => ({ + " WHERE y.short_form IN neurons AND" + " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")" + " AND none(rel in relationships(p) WHERE endNode(rel) = x OR startNode(rel) = y)" - + " WITH root, relationships(p) as fu, p AS pp" + + " WITH root, relationships(p) as fu, p AS pp LIMIT 25" + " UNWIND fu as r" + " WITH root, startNode(r) AS a, endNode(r) AS b, pp, id(r) as id" + " MATCH p=(a)<-[:synapsed_to]-(b)" From 7003e641d61832728e4cae17c71859604bf6a62b Mon Sep 17 00:00:00 2001 From: Rob Court Date: Thu, 25 Mar 2021 10:09:19 +0000 Subject: [PATCH 13/80] adding delays for tree to populate --- tests/jest/vfb/review/tree-browser-tests.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/jest/vfb/review/tree-browser-tests.js b/tests/jest/vfb/review/tree-browser-tests.js index 2a5d1c207..90aef9135 100644 --- a/tests/jest/vfb/review/tree-browser-tests.js +++ b/tests/jest/vfb/review/tree-browser-tests.js @@ -101,6 +101,7 @@ describe('VFB Tree Browser Component Tests', () => { it('Expand node "adult cerebral ganglion"', async () => { // Click on third node of tree browser, 'adult cerebrum' await expandTreeNode(page, "adult central brain"); + await page.waitFor(5000); // Check tree now expanded with adult cerebral ganglion name let element = await findElementByText(page, "adult cerebrum"); expect(element).toEqual("adult cerebrum"); @@ -109,6 +110,7 @@ describe('VFB Tree Browser Component Tests', () => { it('Expand node "adult cerebrum"', async () => { // Click on third node of tree browser, 'adult cerebral ganglion' await expandTreeNode(page, "adult cerebrum"); + await page.waitFor(5000); // Test node for 'adult central brain' exists let element = await findElementByText(page, "adult deutocerebrum"); expect(element).toEqual("adult deutocerebrum"); @@ -116,6 +118,7 @@ describe('VFB Tree Browser Component Tests', () => { it('Click on Node "adult deutocerebrum"', async () => { await clickTreeNode(page, "adult deutocerebrum"); + await page.waitFor(5000); // Check Term Info is now populated with adult cerebral ganglion name let element = await findElementByText(page, "adult deutocerebrum"); expect(element).toBe("adult deutocerebrum"); From 6cad9ff5633b921f2352dfb177ca39f56236e2ec Mon Sep 17 00:00:00 2001 From: Rob Court Date: Thu, 25 Mar 2021 10:12:34 +0000 Subject: [PATCH 14/80] bringing tree test back online --- tests/jest/vfb/{review => batch2}/tree-browser-tests.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/jest/vfb/{review => batch2}/tree-browser-tests.js (100%) diff --git a/tests/jest/vfb/review/tree-browser-tests.js b/tests/jest/vfb/batch2/tree-browser-tests.js similarity index 100% rename from tests/jest/vfb/review/tree-browser-tests.js rename to tests/jest/vfb/batch2/tree-browser-tests.js From d32dda4eb09a640b55bf3f52a8fb78ac8609a907 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Thu, 25 Mar 2021 11:11:35 +0000 Subject: [PATCH 15/80] allowing for multiple --- tests/jest/vfb/batch2/tree-browser-tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/jest/vfb/batch2/tree-browser-tests.js b/tests/jest/vfb/batch2/tree-browser-tests.js index 90aef9135..0b01af949 100644 --- a/tests/jest/vfb/batch2/tree-browser-tests.js +++ b/tests/jest/vfb/batch2/tree-browser-tests.js @@ -135,7 +135,7 @@ describe('VFB Tree Browser Component Tests', () => { it('Click on "eye" icon to render "adult mushroom body" mesh', async () => { await clickNodeIcon(page, "adult mushroom body", 'fa-eye-slash'); // Wait for 'color picker' selector to show, this is the sign that the click on the eye button worked and the mesh was rendered - await wait4selector(page, 'i.fa-tint', { visible: true, timeout : 500000 }); + await wait4selector(page, 'i.fa-tint:first', { visible: true, timeout : 500000 }); }) it('Mesh for "adult mushroom body" rendered in canvas after clicking on eye icon next to node', async () => { From b63ec6ac2632f56475823640d75c34ce653a5066 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Mon, 29 Mar 2021 14:33:47 +0100 Subject: [PATCH 16/80] updating colour --- .../circuitBrowserConfiguration.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 3f96544c5..8840b9877 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -48,7 +48,7 @@ var styling = { canvasColor : "black", // Color for links between nodes defaultLinkColor : "white", - // Color apply to links while hovering over them + // Color apply to links while hovering over them defaultLinkHoverColor : "#11bffe", // Color apply to target and source nodes when hovering over a link or a node. defaultNeighborNodesHoverColor : "orange", @@ -59,39 +59,39 @@ var styling = { // Node border color defaultBorderColor : "black", // When hovering over a node, the node's border color changes to create a halo effect - defaultNodeHoverBoderColor : "red", + defaultNodeHoverBoderColor : "red", // Title bar (in node) background color defaultNodeTitleBackgroundColor : "#11bffe", // Description area (in node) background color defaultNodeDescriptionBackgroundColor : "white", nodeColorsByLabel : { "Template" : "#ff6cc8", - "Ganglion" : "#d6007d", - "Neuromere" : "#d6007d", "GABAergic" : "#9551ff", - "Dopaminergic" : "#9551ff", - "Cholinergic" : "#9551ff", - "Glutamatergic" : "#9551ff", - "Octopaminergic" : "#9551ff", - "Serotonergic" : "#9551ff", - "Motor_neuron" : "#ff6a3a", - "Sensory_neuron" : "#ff6a3a", - "Peptidergic_neuron" : "#ff6a3a", - "Glial_cell" : "#ff6a3a", + "Dopaminergic" : "#3551ff", + "Cholinergic" : "#95515f", + "Glutamatergic" : "#95f1ff", + "Octopaminergic" : "#f3511f", + "Serotonergic" : "#9501f0", + "Motor_neuron" : "#fffa30", + "Sensory_neuron" : "#ff3a3a", + "Peptidergic_neuron" : "#5f6a3a", + "Glial_cell" : "#ff3a6a", "Cell" : "#ff6a3a", "Clone" : "#d6007d", "Synaptic_neuropil" : "#00a2aa", "License" : "#0164d8", "Person" : "#023f00", "Neuron" : "#7f2100", - "Neuron_projection_bundle" : "#d6007d", + "Neuron_projection_bundle" : "#d6327d", "Resource" : "#005f1d", "Site" : "#005f1d", "Expression_pattern" : "#534700", "Split" : "#e012e3", "DataSet" : "#b700b5", - "Anatomy" : "#00a2aa", + "Ganglion" : "#d6007d", + "Neuromere" : "#d6507d", "Property" : "#005f1d", + "Anatomy" : "#00a2aa", "_Class" : "#0164d8" }, controlIcons : { From b06ebe088be51ae7e2724ac7cf201426b1031360 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Mon, 29 Mar 2021 16:20:56 +0100 Subject: [PATCH 17/80] adding length ordering and limit notification --- .../VFBCircuitBrowser/circuitBrowserConfiguration.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 8840b9877..1b9405396 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -1,4 +1,4 @@ -var locationCypherQuery = ( instances, hops, weight ) => ({ +var locationCypherQuery = ( instances, hops, weight, limit = 25 ) => ({ "statements": [ { "statement" : "WITH [" + instances + "] AS neurons" @@ -7,13 +7,13 @@ var locationCypherQuery = ( instances, hops, weight ) => ({ + " WHERE y.short_form IN neurons AND" + " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")" + " AND none(rel in relationships(p) WHERE endNode(rel) = x OR startNode(rel) = y)" - + " WITH root, relationships(p) as fu, p AS pp LIMIT 25" + + " WITH root, relationships(p) as fu, p AS pp, length(p) as l ORDER BY l Asc LIMIT " + limit.toString() + " UNWIND fu as r" + " WITH root, startNode(r) AS a, endNode(r) AS b, pp, id(r) as id" + " MATCH p=(a)<-[:synapsed_to]-(b)" - + " RETURN root, collect(distinct pp) as pp, collect(distinct p) as p, collect(distinct id) as fr", + + " RETURN root, collect(distinct pp) as pp, collect(distinct p) as p, collect(distinct id) as fr," + + " count(pp) >= " + limit.toString() + " as limited", "resultDataContents": ["row", "graph"] - } ] }); From 6f9eba87a2c6d7368cbd0074bd3a61720fc5ed70 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Mon, 29 Mar 2021 17:00:48 +0100 Subject: [PATCH 18/80] fint fix --- .../VFBCircuitBrowser/circuitBrowserConfiguration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 1b9405396..e65bfbe5e 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -12,7 +12,7 @@ var locationCypherQuery = ( instances, hops, weight, limit = 25 ) => ({ + " WITH root, startNode(r) AS a, endNode(r) AS b, pp, id(r) as id" + " MATCH p=(a)<-[:synapsed_to]-(b)" + " RETURN root, collect(distinct pp) as pp, collect(distinct p) as p, collect(distinct id) as fr," - + " count(pp) >= " + limit.toString() + " as limited", + + " count(pp) >= " + limit.toString() + " as limited", "resultDataContents": ["row", "graph"] } ] From 574e60d70e87e4e15a24a98d0fff3cba78b49e56 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Mon, 29 Mar 2021 17:51:14 +0100 Subject: [PATCH 19/80] moving down cell --- .../VFBCircuitBrowser/circuitBrowserConfiguration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index e65bfbe5e..a60123e0f 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -76,7 +76,6 @@ var styling = { "Sensory_neuron" : "#ff3a3a", "Peptidergic_neuron" : "#5f6a3a", "Glial_cell" : "#ff3a6a", - "Cell" : "#ff6a3a", "Clone" : "#d6007d", "Synaptic_neuropil" : "#00a2aa", "License" : "#0164d8", @@ -90,6 +89,7 @@ var styling = { "DataSet" : "#b700b5", "Ganglion" : "#d6007d", "Neuromere" : "#d6507d", + "Cell" : "#ff6a3a", "Property" : "#005f1d", "Anatomy" : "#00a2aa", "_Class" : "#0164d8" From f15834fe6c5936f2492865498e04ad4525683cda Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 30 Mar 2021 10:53:47 -0700 Subject: [PATCH 20/80] #1075 Fix weight label alignment and background color --- components/interface/VFBCircuitBrowser/Controls.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index 8b0b1c1d9..9f393f2dc 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -90,6 +90,9 @@ const styles = theme => ({ marginRight : "5vh", height : "2vh", width : "2vh" + }, + weightInput : { + color : "white !important" } }); @@ -438,12 +441,12 @@ class Controls extends Component { /> - + Weight - +
{ this.props.resultsAvailable() - ?