diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..82c96d8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "parserOptions": { + "ecmaVersion": 6 + }, + "extends": "google" +} diff --git a/.gitignore b/.gitignore index c465040..4521803 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,58 @@ - -# Compiled source # -################### -*.com -*.class -*.dll -*.exe -*.o -*.so -*.pyc - -# Packages # -############ -# it's better to unpack these files and commit the raw source -# git has its own built in compression methods -*.7z -*.dmg -*.gz -*.iso -*.jar -*.rar -*.tar -*.zip - -# Logs and databases # -###################### +# Logs +logs *.log -*.sql -*.db -*.sqlite -*.gexf -*.json - -# OS generated files # -###################### -.DS_Store -.DS_Store? -.Spotlight-V100 -.Trashes -data/* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/README.md b/README.md index cff8c10..7d9540a 100755 --- a/README.md +++ b/README.md @@ -2,21 +2,20 @@ ### Presentation -Thank you for using ProjectExplorer/TinawebJS. +Thank you for using Tinaweb. This work is lead by the Complex Systems Institute of Paris Ile-de-France ([ISCPIF](http://iscpif.fr)) and the [Centre d'Analyse et de Mathématique Sociales](http://cams.ehess.fr/), both [CNRS](http://www.cnrs.fr/) entities. ###### Source code repository - https://github.com/moma/ProjectExplorer + https://github.com/ISCPIF/tinaweb ###### Authors -Researchers and engineers of the [ISCPIF/CNRS - UPS 3611](http://iscpif.fr) - - Dr. David Chavalarias +This work is conducted under the supervision of Dr. David Chavalarias (CNRS, CAMS & ISC-PIF), with the following contributors to the development : - Samuel Castillo - Romain Loth -You can contact the authors by email (). +You can contact us by email (). ###### Acknowledgements - TinawebJS is build on top of Alexis Jacomy and Guillaume Plique's [sigmaJS](http://sigmajs.org) @@ -28,21 +27,21 @@ You can contact the authors by email (). ### Usage -ProjectExplorer is a versatile app that can be used as standalone or as a client library. The documentation concerning the different setup cases is being updated after a major refactoring and will grow in time. +Tinaweb is a versatile app that can be used as standalone or as a client library. The documentation concerning the different setup cases is being updated after a major refactoring and will grow in time. Here are the main points. ###### Getting started In the simplest setup, just clone the repository and open explorerjs.html in a modern browser. ``` -git clone https://github.com/moma/ProjectExplorer.git -cd ProjectExplorer +git clone git@github.com:ISCPIF/tinaweb.git +cd tinaweb firefox explorerjs.html ``` => An input in the upper right side allows you to open any gexf file. [] ###### Application and data structures -An overview of the application structure can be found in the extended documentation under [00.DOCUMENTATION/A-Introduction/app_structure.md](https://github.com/moma/ProjectExplorer/blob/master/00.DOCUMENTATION/A-Introduction/app_structure.md) (comments are in french). +An overview of the application structure can be found in the extended documentation under [00.DOCUMENTATION/A-Introduction/app_structure.md](https://github.com/ISCPIF/tinaweb/blob/master/00.DOCUMENTATION/A-Introduction/app_structure.md) (comments are in french). ###### Usage on a web server To activate all features, you should: @@ -59,14 +58,14 @@ Once you have this webserver running and some source data files, you may also co - it will be shown as a **menu** to select graphs in the interface - it allows to define associated **node types** for each source - it allows to define associated **search backends** for each source - - to use this, follow the guidelines in the **[Project config HOWTO](https://github.com/moma/ProjectExplorer/blob/master/00.DOCUMENTATION/A-Introduction/project_config.md)** + - to use this, follow the guidelines in the **[Project config HOWTO](https://github.com/ISCPIF/tinaweb/blob/master/00.DOCUMENTATION/A-Introduction/project_config.md)** ###### Integration in a larger app -To integrate ProjectExplorer in a larger web application, you may have several locations with subdirectories defined on your server. In this case, you'll need to use the provided path modification tool (see this [integration procedure example](https://github.com/moma/ProjectExplorer/tree/master/00.DOCUMENTATION/A-Introduction#integration-policy)) +To integrate ProjectExplorer in a larger web application, you may have several locations with subdirectories defined on your server. In this case, you'll need to use the provided path modification tool (see this [integration procedure example](https://github.com/ISCPIF/tinaweb/tree/master/00.DOCUMENTATION/A-Introduction#integration-policy)) ###### Advanced settings -For more information about ProjectExplorer's settings (settings file, input modes, attribute processing options), please refer to the [detailed introduction](https://github.com/moma/ProjectExplorer/blob/master/00.DOCUMENTATION/A-Introduction/README.md) - and the [developer's manual](https://github.com/moma/ProjectExplorer/blob/master/00.DOCUMENTATION/C-advanced/developer_manual.md). +For more information about ProjectExplorer's settings (settings file, input modes, attribute processing options), please refer to the [detailed introduction](https://github.com/ISCPIF/tinaweb/blob/master/00.DOCUMENTATION/A-Introduction/README.md) + and the [developer's manual](https://github.com/ISCPIF/tinaweb/blob/master/00.DOCUMENTATION/C-advanced/developer_manual.md). ### Copyright and license diff --git a/package.json b/package.json new file mode 100644 index 0000000..ffe1c83 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "tinaweb", + "version": "1.0.0", + "description": "TODO", + "main": "settings_explorerjs.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint **/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ISCPIF/tinaweb.git" + }, + "author": "", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/ISCPIF/tinaweb/issues" + }, + "homepage": "https://github.com/ISCPIF/tinaweb#readme", + "devDependencies": { + "eslint": "^4.9.0", + "eslint-config-google": "^0.9.1" + } +} diff --git a/twmain/Tinaweb.js b/twmain/Tinaweb.js index 9bff6e7..4a3ca9e 100644 --- a/twmain/Tinaweb.js +++ b/twmain/Tinaweb.js @@ -1,1345 +1,1276 @@ 'use strict'; -// this class will be instanciated once (and exposed as TW.instance.selNgn) -function SelectionEngine() { - - // "main" selection function for a typical event - // - checks the cases - // - calls the other SelectionEngine routines - // - triggers GUI effects and state - this.runAndEffects = function (eventTargets) { - // 1) - var targeted = this.SelectorEngine( { - addvalue:TW.gui.checkBox, - currsels: eventTargets, - prevsels: TW.SystemState().selectionNids - } ) - // 2) - if(targeted.length>0) { - - // we still check if the selection is unchanged before create state - let currentNids = TW.SystemState().selectionNids - let sameNids = true - if (currentNids.length != targeted.length) { - sameNids = false - } - else { - for (var j in currentNids) { - if (currentNids[j] != targeted[j]) { - sameNids = false - break - } +/** + * This class will be instanciated once (and exposed as TW.instance.selNgn). + */ +class SelectionEngine { + /** + * "main" selection function for a typical event + * - checks the cases + * - calls the other SelectionEngine routines + * - triggers GUI effects and state + */ + runAndEffects(eventTargets) { + let targeted = this.selectorEngine({ + addvalue: TW.gui.checkBox, + currsels: eventTargets, + prevsels: TW.SystemState().selectionNids, + }); + + if (targeted.length > 0) { + // we still check if the selection is unchanged before create state + let currentNids = TW.SystemState().selectionNids; + let sameNids = true; + if (currentNids.length != targeted.length) { + sameNids = false; + } else { + for (let j in currentNids) { + if (currentNids[j] != targeted[j]) { + sameNids = false; + break; } } - - // iff new selection, create effects and state - if (!sameNids) { - this.MultipleSelection2( {nodes:targeted} ) - } } - // or clear previous selection, also with effects and state - else { - cancelSelection(false) + + // iff new selection, create effects and state + if (!sameNids) { + this.multipleSelection2({ nodes: targeted }); } + } else { // or clear previous selection, also with effects and state + cancelSelection(false); + } + } + + /** + * Creates the union of prevsels and currsels, if addvalue. + * Called for: + * clickNode selections + * circleArea selections + * searchInput selections + */ + selectorEngine(args) { + // console.log("addvalue, prevsels, currsels", args) + + if (!args) args = {}; + if (!args.addvalue) args.addvalue = false; + if (!args.prevsels) args.prevsels = []; + if (!args.currsels) args.currsels = []; + + let targeted = []; + + // currsels = bunch of nodes from a click in the map + if (args.addvalue) { + // complementary select if disjoint, deselect if overlap + targeted = args.currsels.filter(function(item) { + return args.prevsels.indexOf(item) < 0; + }).concat(args.prevsels.filter(function(item) { + return args.currsels.indexOf(item) < 0; + })); + } else if (TW.gui.circleSize) { // circle select default: deselect if overlap + targeted = args.currsels.filter(function(item) { + return args.prevsels.indexOf(item) < 0; + }); + } else { // other view default: only new targets + targeted = args.currsels; } - // creates the union of prevsels and currsels, if addvalue - // called for: - // clickNode selections - // circleArea selections - // searchInput selections - this.SelectorEngine = function( args ) { - - // console.log("addvalue, prevsels, currsels", args) - - if (!args) args = {} - if (!args.addvalue) args.addvalue = false - if (!args.prevsels) args.prevsels = [] - if (!args.currsels) args.currsels = [] - - var targeted = [] - - // currsels = bunch of nodes from a click in the map - if(args.addvalue) { - // complementary select if disjoint, deselect if overlap - targeted = args.currsels.filter(function (item) { - return args.prevsels.indexOf(item) < 0; - }).concat(args.prevsels.filter(function (item) { - return args.currsels.indexOf(item) < 0; - })); - } - // circle select default: deselect if overlap - else if (TW.gui.circleSize) { - targeted = args.currsels.filter(function (item) { - return args.prevsels.indexOf(item) < 0; - }); - } - // other view default: only new targets - else { - targeted = args.currsels; - } - - if(targeted.length==0) return []; - - return targeted; - }; - - - // uses: SelectorEngine() and MultipleSelection2() - // we assume string is normalized - this.search_n_select = function(string) { + if (targeted.length == 0) return []; - let previousSelections = TW.SystemState().selectionNids + return targeted; + }; - cancelSelection(false, {norender:true}); + // uses: selectorEngine() and multipleSelection2() + // we assume string is normalized + searchNSelect(string) { + let previousSelections = TW.SystemState().selectionNids; - if (typeof string != "string") { - return -1 ; - } - else if (string.length == 0) { - // alert("really? empty query?") ; - return 0 ; - } - else { - var id_node = ''; - var resultNids = find(string) + cancelSelection(false, {norender: true}); - var coincd=[] - for(var i in resultNids) { - coincd.push(resultNids[i]) - } - var targeted = this.SelectorEngine( { - addvalue:TW.gui.checkBox, - prevsels:previousSelections, - currsels:coincd - } ) + if (typeof string != 'string') { + return -1; + } else if (string.length == 0) { + return 0; + } else { + let resultNids = find(string); - if(targeted.length>0) { - // got results - // ----------- - this.MultipleSelection2({nodes:targeted}); + let coincd = []; + for (let nid of resultNids) { + coincd.push(nid); + } + let targeted = this.selectorEngine({ + addvalue: TW.gui.checkBox, + prevsels: previousSelections, + currsels: coincd, + }); - } - else { - // no results - // ---------- - // we send event for plugins - // (eg: propose to add the missing string to the graph) - $('#searchinput').trigger({ - type: "tw:emptyNodeSet", - q: string, - nodeIds: [] - }); - // console.log("\n\n\n sent [[ emptyNodes ]] \n\n ") - } + if (targeted.length > 0) { + // got results + // ----------- + this.multipleSelection2({nodes: targeted}); + } else { + // no results + // ---------- + // we send event for plugins + // (eg: propose to add the missing string to the graph) + $('#searchinput').trigger({ + type: 'tw:emptyNodeSet', + q: string, + nodeIds: [], + }); + } - // anyway - // ------ - TW.lastQuery = string ; - $("input#searchinput").val(""); - $("input#searchinput").autocomplete( "close" ); + // anyway + // ------ + TW.lastQuery = string; + $('input#searchinput').val(''); + $('input#searchinput').autocomplete('close'); - return targeted.length - } + return targeted.length; } - - //Util - this.intersect_safe = function(a, b) { - a.sort() - b.sort() - var ai=0, bi=0; - var result = new Array(); - - while( ai < a.length && bi < b.length ) { - if (a[ai] < b[bi] ){ ai++; } - else if (a[ai] > b[bi] ){ bi++; } - else /* they're equal */ { - result.push(a[ai]); - ai++; - bi++; - } - } - return result; + } + + /** + * Main function for any selecting action + * + * @nodes: eg targeted array (only ids) + * @noState: bool flag to avoid registering new state (useful for CTRL+Z) + * + * external usage : clickHandler, search, changeType, filters, tag click... + */ + // ==================== + multipleSelection2(args) { + if (!args) args = {}; + if (isUndef(args.nodes)) args.nodes = []; + if (isUndef(args.noState)) args.noState = false; + let tMS2Start; + + if (TW.conf.debug.logSelections) { + tMS2Start = performance.now(); + console.log( + 'IN SelectionEngine.multipleSelection2:', args.nodes, + 'noState:', args.noState + ); } - /** - * Main function for any selecting action - * - * @nodes: eg targeted array (only ids) - * @noState: bool flag to avoid registering new state (useful for CTRL+Z) - * - * external usage : clickHandler, search, changeType, filters, tag click... - */ - // ==================== - this.MultipleSelection2 = function(args) { - - if (!args) args = {} - if (isUndef(args.nodes)) args.nodes = [] - if (isUndef(args.noState)) args.noState = false - - if (TW.conf.debug.logSelections) { - var tMS2_deb = performance.now() - console.log( - "IN SelectionEngine.MultipleSelection2:", args.nodes, - "noState:", args.noState - ) - } - - // deselects only the active ones (based on SystemState()) - deselectNodes() - - // TW.SystemState() is the present graph state - // eg - // {categories: ["someNodeCat"] - // categoriesDict: {"someNodeCat":0} // where val 0 or 1 is type sem or soc - // opposites: - // selections: - // type:[0]} - - - var activereltypes = TW.SystemState().activereltypes - + // deselects only the active ones (based on SystemState()) + deselectNodes(); - // Dictionaries of: selection+neighbors for the new state and updateRelatedNodesPanel - let selections = {} + // TW.SystemState() is the present graph state + // eg + // {categories: ["someNodeCat"] + // categoriesDict: {"someNodeCat":0} // where val 0 or 1 is type sem or soc + // opposites: + // selections: + // type:[0]} - // detailed relations sorted by types and srcid (for state cache, deselects etc) - let activeRelations = {} - for (var k in activereltypes) { - let activereltype = activereltypes[k] - activeRelations[activereltype] = {} - } - - // cumulated neighbor weights no matter what srcid (for tagCloud etc) - let sameSideNeighbors = {} - let oppoSideNeighbors = {} - - // targeted arg 'nodes' can be nid array or single nid - var ndsids=[] - if(args.nodes) { - if(! $.isArray(args.nodes)) ndsids.push(args.nodes); - else ndsids=args.nodes; - - for(var i in ndsids) { - var srcnid = ndsids[i]; - - - for (var k in activereltypes) { - let activereltype = activereltypes[k] - - if(TW.Relations[activereltype] && TW.Relations[activereltype][srcnid] ) { - var neighs = TW.Relations[activereltype][srcnid] - - activeRelations[activereltype][srcnid] = {} - - if(neighs) { - for(var j in neighs) { - var tgtnid = neighs[j] + let activereltypes = TW.SystemState().activereltypes; - let tgt = TW.partialGraph.graph.nodes(tgtnid) - // highlight edges (except if n hidden or e dropped (<=> lock)) - // POSS: use sigma's own index to avoid checking if edge dropped - if (tgt && !tgt.hidden) { - let eid1 = srcnid+';'+tgtnid - let eid2 = tgtnid+';'+srcnid + // Dictionaries of: selection+neighbors for the new state and updateRelatedNodesPanel + let selections = {}; - if ( (TW.Edges[eid1] && !TW.Edges[eid1].lock) - || - (TW.Edges[eid2] && !TW.Edges[eid2].lock) ) { + // detailed relations sorted by types and srcid (for state cache, deselects etc) + let activeRelations = {}; - let e1 = TW.partialGraph.graph.edges(eid1) - let e2 = TW.partialGraph.graph.edges(eid2) - - // since we're there we'll also keep the neighbors info - if (typeof sameSideNeighbors[tgtnid] == 'undefined') { - - // except when XR because it'll already be in oppoSideNeighbors - if (activereltype != 'XR') - sameSideNeighbors[tgtnid]=0 - } - - // and the detailed info - if (typeof activeRelations[activereltype][srcnid][tgtnid] == 'undefined') { - activeRelations[activereltype][srcnid][tgtnid]=0 - } + for (let activereltype of activereltypes) { + activeRelations[activereltype] = {}; + } - // **make the edge active** - if (e1 && !e1.hidden) { - e1.customAttrs.activeEdge = 1; - activeRelations[activereltype][srcnid][tgtnid] += e1.weight || 1 + // cumulated neighbor weights no matter what srcid (for tagCloud etc) + let sameSideNeighbors = {}; + let oppoSideNeighbors = {}; + + // targeted arg 'nodes' can be nid array or single nid + let ndsids = []; + if (args.nodes) { + if (! $.isArray(args.nodes)) ndsids.push(args.nodes); + else ndsids = args.nodes; + + for (let srcnid of ndsids) { + for (let activereltype of activereltypes) { + if (TW.Relations[activereltype] && TW.Relations[activereltype][srcnid] ) { + let neighs = TW.Relations[activereltype][srcnid]; + + activeRelations[activereltype][srcnid] = {}; + + if (neighs) { + for (let tgtnid of neighs) { + let tgt = TW.partialGraph.graph.nodes(tgtnid); + // highlight edges (except if n hidden or e dropped (<=> lock)) + // POSS: use sigma's own index to avoid checking if edge dropped + if (tgt && !tgt.hidden) { + let eid1 = srcnid+';'+tgtnid; + let eid2 = tgtnid+';'+srcnid; + + if ((TW.Edges[eid1] && !TW.Edges[eid1].lock) + || + (TW.Edges[eid2] && !TW.Edges[eid2].lock)) { + let e1 = TW.partialGraph.graph.edges(eid1); + let e2 = TW.partialGraph.graph.edges(eid2); + + // since we're there we'll also keep the neighbors info + if (typeof sameSideNeighbors[tgtnid] == 'undefined') { + // except when XR because it'll already be in oppoSideNeighbors + if (activereltype != 'XR') { + sameSideNeighbors[tgtnid] = 0; + } + } - // + enrich neighbor's info except if duplicate with oppoSideNeighbors - if (activereltype != 'XR') - sameSideNeighbors[tgtnid] += e1.weight || 1 - } - if (e2 && !e2.hidden) { - e2.customAttrs.activeEdge = 1; - activeRelations[activereltype][srcnid][tgtnid] += e2.weight || 1 + // and the detailed info + if (typeof activeRelations[activereltype][srcnid][tgtnid] == 'undefined') { + activeRelations[activereltype][srcnid][tgtnid] = 0; + } - // + enrich neighbor's info except if duplicate with oppoSideNeighbors - if (activereltype != 'XR') - sameSideNeighbors[tgtnid] += e2.weight || 1 - } + // **make the edge active** + if (e1 && !e1.hidden) { + e1.customAttrs.activeEdge = 1; + activeRelations[activereltype][srcnid][tgtnid] += e1.weight || 1; - // we add as neighbor to color it (except if already in targeted) - if (!tgt.customAttrs.active) tgt.customAttrs.highlight = 1; - } - } - } + // + enrich neighbor's info except if duplicate with oppoSideNeighbors + if (activereltype != 'XR') { + sameSideNeighbors[tgtnid] += e1.weight || 1; } - } + } + if (e2 && !e2.hidden) { + e2.customAttrs.activeEdge = 1; + activeRelations[activereltype][srcnid][tgtnid] += e2.weight || 1; + + // + enrich neighbor's info except if duplicate with oppoSideNeighbors + if (activereltype != 'XR') { + sameSideNeighbors[tgtnid] += e2.weight || 1; + } + } + // we add as neighbor to color it (except if already in targeted) + if (!tgt.customAttrs.active) tgt.customAttrs.highlight = 1; + } } - - // we make the selected (source) node active too - let src = TW.partialGraph.graph.nodes(srcnid) - src.customAttrs.active = true; - - // update local selections dict - selections[ndsids[i]]=1; + } } + } } - // show the button to remove selection - $("#unselectbutton").show() ; + // we make the selected (source) node active too + let src = TW.partialGraph.graph.nodes(srcnid); + src.customAttrs.active = true; - let theSelection = Object.keys(selections) - - // neighbors of the opposite type - if(TW.Relations["XR"]) { + // update local selections dict + selections[ndsids[i]]=1; + } + } - activeRelations["XR"] = {} + // show the button to remove selection + $('#unselectbutton').show(); - for(var i in theSelection) { - let srcnid = theSelection[i] + let theSelection = Object.keys(selections); - var bipaNeighs = TW.Relations["XR"][srcnid]; + // neighbors of the opposite type + if (TW.Relations['XR']) { + activeRelations['XR'] = {}; - activeRelations["XR"][srcnid] = {} + for (let srcnid of theSelection) { + let bipaNeighs = TW.Relations['XR'][srcnid]; - for(var k in bipaNeighs) { + activeRelations['XR'][srcnid] = {}; - let eid1 = srcnid+';'+bipaNeighs[k] - let eid2 = bipaNeighs[k]+';'+srcnid + for (let neigh of bipaNeighs) { + let eid1 = srcnid + ';' + neigh; + let eid2 = neigh + ';' + srcnid; - var edgeWeight = 1 - if (TW.Edges[eid1]) edgeWeight = TW.Edges[eid1].weight - else if (TW.Edges[eid2]) edgeWeight = TW.Edges[eid2].weight + let edgeWeight = 1; + if (TW.Edges[eid1]) edgeWeight = TW.Edges[eid1].weight; + else if (TW.Edges[eid2]) edgeWeight = TW.Edges[eid2].weight; - if (typeof activeRelations["XR"][srcnid][bipaNeighs[k]] == "undefined") { - activeRelations["XR"][srcnid][bipaNeighs[k]] = 0; - } - if (typeof oppoSideNeighbors[bipaNeighs[k]] == "undefined") { - oppoSideNeighbors[bipaNeighs[k]] = 0 ; - } + if (typeof activeRelations['XR'][srcnid][neigh] == 'undefined') { + activeRelations['XR'][srcnid][neigh] = 0; + } + if (typeof oppoSideNeighbors[neigh] == 'undefined') { + oppoSideNeighbors[neigh] = 0; + } - // cumulated weight for all srcnids - oppoSideNeighbors[bipaNeighs[k]] += edgeWeight + // cumulated weight for all srcnids + oppoSideNeighbors[neigh] += edgeWeight; - // console.log('edgeWeight', edgeWeight) + // console.log('edgeWeight', edgeWeight) - // and the details - activeRelations["XR"][srcnid][bipaNeighs[k]] += edgeWeight - } - } + // and the details + activeRelations['XR'][srcnid][neigh] += edgeWeight; } + } + } - // Sort by descending value - // and rewrites dict[id]: val as array[order]: {key:id, value:val} - let oppos = [] - let same = [] + // Sort by descending value + // and rewrites dict[id]: val as array[order]: {key:id, value:val} + let oppos = []; + let same = []; - if (activeRelations["XR"]) { - oppos = ArraySortByValue(oppoSideNeighbors, function(a,b){ - return b-a - }); - } + if (activeRelations['XR']) { + oppos = ArraySortByValue(oppoSideNeighbors, function(a, b) { + return b-a; + }); + } - same = ArraySortByValue(sameSideNeighbors, function(a,b){ - return b-a - }); + same = ArraySortByValue(sameSideNeighbors, function(a, b) { + return b-a; + }); - if (TW.conf.debug.logSelections) { - console.log('new states\'s selectionNids', theSelection) - console.log('oppos', oppos) - console.log('same', same) - } + if (TW.conf.debug.logSelections) { + console.log('new states\'s selectionNids', theSelection); + console.log('oppos', oppos); + console.log('same', same); + } - // it's a new SystemState - if (! args.noState) { - TW.pushGUIState( { 'sels': theSelection, - 'rels': activeRelations } ) - } + // it's a new SystemState + if (! args.noState) { + TW.pushGUIState({'sels': theSelection, + 'rels': activeRelations}); + } - // we send our "gotNodeSet" event - // (signal for plugins that a search-selection was done or a new hand picked selection) - $('#searchinput').trigger({ - type: "tw:gotNodeSet", - q: $("#searchinput").val(), - nodeIds: theSelection - }); + // we send our "gotNodeSet" event + // (signal for plugins that a search-selection was done or a new hand picked selection) + $('#searchinput').trigger({ + type: 'tw:gotNodeSet', + q: $('#searchinput').val(), + nodeIds: theSelection, + }); - // global flag - TW.gui.selectionActive = true + // global flag + TW.gui.selectionActive = true; - TW.partialGraph.render(); + TW.partialGraph.render(); - updateRelatedNodesPanel( theSelection , same, oppos ) + updateRelatedNodesPanel(theSelection, same, oppos); - if (TW.conf.debug.logSelections) { - var tMS2_fin = performance.now() - console.log("end MultipleSelection2, own time:", tMS2_fin-tMS2_deb) - } + if (TW.conf.debug.logSelections) { + let tMS2End = performance.now(); + console.log('end multipleSelection2, own time:', tMS2End-tMS2Start); } -}; + } +} -var TinaWebJS = function ( sigmacanvas ) { +class TinaWebJS { + constructor(sigmacanvas) { this.sigmacanvas = sigmacanvas; this.selNgn = new SelectionEngine(); + } - // functions that modify the sigma module (not sigma instance!) - this.init = function () { + // functions that modify the sigma module (not sigma instance!) + init() { + if (TW.conf.debug.logSettings) console.info('TW settings', TW); - if (TW.conf.debug.logSettings) console.info("TW settings", TW) + let initErrMsg = null; - let initErrMsg = null + if (typeof sigma == 'undefined') { + initErrMsg = 'no sigma library'; + } else { + this.prepareSigmaCustomIndices(sigma); - if (typeof sigma == 'undefined') { - initErrMsg = "no sigma library" - } - else { - this.prepareSigmaCustomIndices(sigma) - - if (TW.conf.twRendering) - this.prepareSigmaCustomRenderers(sigma) - } + if (TW.conf.twRendering) { +this.prepareSigmaCustomRenderers(sigma); +} + } - // overriding pixelRatio is possible if we need high definition - if (TW.conf.overSampling) { - var realRatio = sigma.utils.getPixelRatio - sigma.utils.getPixelRatio = function() { - return 2 * realRatio() - } - } + // overriding pixelRatio is possible if we need high definition + if (TW.conf.overSampling) { + let realRatio = sigma.utils.getPixelRatio; + sigma.utils.getPixelRatio = function() { + return 2 * realRatio(); + }; + } - // show any already existing panel - document.getElementById("graph-panels").style.display = "block" + // show any already existing panel + document.getElementById('graph-panels').style.display = 'block'; - // grey message in the search bar from settings - $("#searchinput").attr('placeholder', TW.conf.strSearchBar) ; + // grey message in the search bar from settings + $('#searchinput').attr('placeholder', TW.conf.strSearchBar); - // load optional modules - activateModules() ; + // load optional modules + activateModules(); - if (initErrMsg) { - console.error(initErrMsg) - } + if (initErrMsg) { + console.error(initErrMsg); } - - this.prepareSigmaCustomIndices = function(sigmaModule) { - // register direct access methods for nNodes nEdges - sigmaModule.classes.graph.addMethod('nNodes', function() { - return this.nodesArray.length; - }); - sigmaModule.classes.graph.addMethod('nEdges', function() { - return this.edgesArray.length; - }); - - // register an index for nodes by type and size (<= origNode.size||origNode.weight) - sigmaModule.classes.graph.addIndex('nodesByTypeNSize', { - constructor: function() { - this.nodesByTypeNSize = {}; - }, - addNode: function(n) { - // POSS: index by ntypeId = 0 for nodes0 or 1 for nodes1 - if (n.type && n.size) { - let typekey = TW.catDict[n.type] - let sizekey = parseFloat(n.size) - if (!this.nodesByTypeNSize[typekey]) - this.nodesByTypeNSize[typekey] = {} - if (!this.nodesByTypeNSize[typekey][sizekey]) - this.nodesByTypeNSize[typekey][sizekey] = {} - this.nodesByTypeNSize[typekey][sizekey][n.id] = true - } - else { - // should never happen - console.warn("warning: couldn't add node to index ?", n) + } + + prepareSigmaCustomIndices(sigmaModule) { + // register direct access methods for nNodes nEdges + sigmaModule.classes.graph.addMethod('nNodes', function() { + return this.nodesArray.length; + }); + sigmaModule.classes.graph.addMethod('nEdges', function() { + return this.edgesArray.length; + }); + + // register an index for nodes by type and size (<= origNode.size||origNode.weight) + sigmaModule.classes.graph.addIndex('nodesByTypeNSize', { + constructor: function() { + this.nodesByTypeNSize = {}; + }, + addNode: function(n) { + // POSS: index by ntypeId = 0 for nodes0 or 1 for nodes1 + if (n.type && n.size) { + let typekey = TW.catDict[n.type]; + let sizekey = parseFloat(n.size); + if (!this.nodesByTypeNSize[typekey]) { +this.nodesByTypeNSize[typekey] = {}; +} + if (!this.nodesByTypeNSize[typekey][sizekey]) { +this.nodesByTypeNSize[typekey][sizekey] = {}; +} + this.nodesByTypeNSize[typekey][sizekey][n.id] = true; + } else { + // should never happen + console.warn('warning: couldn\'t add node to index ?', n); + } + }, + dropNode: function(n) { + if (n.type && n.size) { + let typekey = TW.catDict[n.type]; + delete(this.nodesByTypeNSize[typekey][n.size][n.id]); + } + }, + }); + + // @typekey: a node type id among {0,1} + // @aSizeSelector can be: + // - a numeric value + // - a range (ordered array of 2 numeric values) + sigmaModule.classes.graph.addMethod('getNodesBySize', function(typekey, aSizeSelector) { + let res = []; + + // shortcut case for commodity: entire index if no arg + if (isUndef(aSizeSelector)) { + res = this.nodesByTypeNSize[typekey]; + } else if (isNumeric(aSizeSelector)) { // normal cases + let sizekey = parseFloat(aSizeSelector); + if (this.nodesByTypeNSize[typekey][sizekey]) { + res = Object.keys(this.nodesByTypeNSize[typekey][sizekey]); + } + } else if (Array.isArray(aSizeSelector) + && aSizeSelector.length == 2 + && isNumeric(aSizeSelector[0]) + && isNumeric(aSizeSelector[1]) + ) { + let sizeMin = parseFloat(aSizeSelector[0]); + let sizeMax = parseFloat(aSizeSelector[1]); + + // the available sizes + let sortedSizes = Object.keys(this.nodesByTypeNSize[typekey]) + .sort(function(a, b) { + return a-b; + }); + + // the nodes with sizes in range + for (let val of sortedSizes) { + if (val > sizeMax) { + break; } - }, - dropNode: function(n) { - if (n.type && n.size) { - let typekey = TW.catDict[n.type] - delete(this.nodesByTypeNSize[typekey][n.size][n.id]) + if (val >= sizeMin) { + res = res.concat(Object.keys(this.nodesByTypeNSize[typekey][val])); } } - }); - - // @typekey: a node type id among {0,1} - // @aSizeSelector can be: - // - a numeric value - // - a range (ordered array of 2 numeric values) - sigmaModule.classes.graph.addMethod('getNodesBySize', function(typekey, aSizeSelector) { - let res = [] + } + return res; + }); + + // All nodes *in the instance* by type + // NB: not used at the moment but easy and perhaps very useful in future + // arg: + // @typekey: a node type id among {0,1} + sigmaModule.classes.graph.addMethod('getNodesByType', function(typekey) { + let res = []; + // concatenate all sizes because this detail doesn't matter to us here + for (let nodes of this.nodesByTypeNSize[typekey]) { + res = res.concat(Object.keys(nodes)); + } + return res; + }); + } + + // register our renderers in sigma module + prepareSigmaCustomRenderers(sigmaModule) { + // £TODO group the rendering primitives it all together + // and perhaps move here where the preparation occurs + let tempo = new SigmaUtils(); + + // custom nodes rendering + // overriding the def is simplest + // (could also do it by type) + sigmaModule.canvas.nodes.def = tempo.twRender.canvas.nodes.withBorders; + + // custom edges rendering registered under 'curve' + sigmaModule.canvas.edges.curve = tempo.twRender.canvas.edges.curve; + sigmaModule.canvas.edges.line = tempo.twRender.canvas.edges.line; + + // custom labels rendering + // - based on the normal one sigma.canvas.labels.def + // - additionnaly supports 'active/highlight' node property (magnify x 3) + // - also handles 'forceLabel' property + sigmaModule.canvas.labels.def = tempo.twRender.canvas.labels.largeractive; + + // custom hovers rendering + // - based on the normal one sigma.canvas.hovers.def + // - additionnaly magnifies all labels x 2 + // - additionnaly supports 'active/highlight' node property (magnify x 3) + sigmaModule.canvas.hovers.def = tempo.twRender.canvas.hovers.largerall; + + if (TW.conf.debug.logSettings) console.log('tw renderers registered in sigma module'); + } + + initSearchListeners() { + let selectionEngine = this.selNgn; + + $('input#searchinput').autocomplete({ + source: function(request, response) { + // labels initialized in settings, filled in updateSearchLabels + // console.log(labels.length) + let matches = []; + let matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), 'i'); + // grep at heart + let results = $.grep(TW.labels, function(e) { + return matcher.test(e.label); // || matcher.test(e.desc); + }); - // shortcut case for commodity: entire index if no arg - if (isUndef(aSizeSelector)) { - res = this.nodesByTypeNSize[typekey] + if (!results.length) { + $('#noresults').text('Pas de résultats'); + } else { + $('#noresults').empty(); } - - // normal cases - else if (isNumeric(aSizeSelector)) { - let sizekey = parseFloat(aSizeSelector) - if (this.nodesByTypeNSize[typekey][sizekey]) { - res = Object.keys(this.nodesByTypeNSize[typekey][sizekey]) - } + matches = results.slice(0, TW.conf.maxSuggestionsAutoComplete); + response(matches); + }, + minLength: TW.conf.minLengthAutoComplete, + + + // ----------------------->8--------------------- + // send a "no more suggestions event" + response: function(event, uiResponseArray) { + // exemple in uiResponseArray + // {"content": + // [{id: 239, label:"warning system",desc:"ISIterms.."}, + // ... + // ] + // } + + if (uiResponseArray.content.length == 0) { + // we send our "noAutocomplete" event + $('#searchinput').trigger('tw:noAutocomplete'); + // (signal for plugins like crowdsourcing that + // are sensitive to autocomplete being empty) + // console.log("000 Event [noAutocomplete] sent from Tinaweb search") + } else { + // we send a "hasAutocomplete" event + // (/!\ will be sent for each typed char) + $('#searchinput').trigger('tw:gotAutocomplete'); + // console.log("+++ Event [gotAutocomplete] sent from Tinaweb search") } - else if (Array.isArray(aSizeSelector) - && aSizeSelector.length == 2 - && isNumeric(aSizeSelector[0]) - && isNumeric(aSizeSelector[1]) - ) { - let sizeMin = parseFloat(aSizeSelector[0]) - let sizeMax = parseFloat(aSizeSelector[1]) - - // the available sizes - let sortedSizes = Object.keys(this.nodesByTypeNSize[typekey]).sort(function(a,b){return a-b}) - - // the nodes with sizes in range - for (var k in sortedSizes) { - let val = sortedSizes[k] - if (val > sizeMax) { - break - } - if (val >= sizeMin) { - res = res.concat(Object.keys(this.nodesByTypeNSize[typekey][val])) - } - } + }, + // ----------------------->8--------------------- + }); + + // Search by click on the search button, independently from autocomplete + $('#searchbutton').click(function() { + let query = normalizeString($('#searchinput').val()); + // console.log('===\nyour query was: "'+query+'"'); + + // --- SelectionEngine.search() ------------------- + // -> will call sigmaUtils.find() + // over sigmaUtils.getnodesIndex() + // -> then call this.selectorEngine + // and this.multipleSelection2 + selectionEngine.searchNSelect(query); + // ------------------------------------------------ + }); + + // Search by pressing ENTER, independently from autocomplete + $('#searchinput').keydown(function(e) { + if (e.keyCode == 13) { + let query = normalizeString($('#searchinput').val()); + selectionEngine.searchNSelect(query); + } + }); + } + + // to init handlers + elts for tina GUI environment (run once on page load) + initGUIListeners() { + let body = document.getElementsByTagName('body')[0]; + body.style.paddingTop = '41px'; + + // side panel width + if (TW.conf.sidePanelSize && TW.conf.sidePanelSize != '400px') { + // change stylesheet rules preferably to element style directly + // (this way we don't block the mobile-variants CSS effects, + // b/c twjs-mobile.css is loaded after twjs.css in the html) + if (TW.gui.sheets) { + if (TW.gui.sheets.main) { + TW.gui.sheets.main.insertRule( + `#sidebar {width: ${TW.conf.sidePanelSize};}`, + TW.gui.sheets.main.cssRules.length + ); + TW.gui.sheets.main.insertRule( + `#sigma-contnr {right: ${TW.conf.sidePanelSize};}`, + TW.gui.sheets.main.cssRules.length + ); } - return res; - }); - - // All nodes *in the instance* by type - // NB: not used at the moment but easy and perhaps very useful in future - // arg: - // @typekey: a node type id among {0,1} - sigmaModule.classes.graph.addMethod('getNodesByType', function(typekey) { - let res = [] - // concatenate all sizes because this detail doesn't matter to us here - for (let szk in this.nodesByTypeNSize[typekey]) { - res = res.concat(Object.keys(this.nodesByTypeNSize[typekey][szk])) + if (TW.gui.sheets.panels) { + TW.gui.sheets.panels.insertRule( + `#ctlzoom {right: calc(${TW.conf.sidePanelSize} + 10px);}`, + TW.gui.sheets.panels.cssRules.length + ); } - return res; - }); + } else { // otherwise we do it the easy way + console.warn('Couldn\'t identify twjs.css and selection-panels.css'); + document.getElementById('sidebar').style.width = TW.conf.sidePanelSize; + document.getElementById('sigma-contnr').style.right = TW.conf.sidePanelSize; + document.getElementById('ctlzoom').style.right = `calc(${TW.conf.sidePanelSize} + 10px)`; + } } + // tab handlers + $('.etabs').click(function() { + setTimeout( + function() { + $('#read-opposite-neighs').readmore({maxHeight: 200}); + $('#read-sameside-neighs').readmore({maxHeight: 200}); + }, + 500 + ); + }); + + $('#changetype').click(function() { + console.log('changeTYPE click'); + if (TW.partialGraph.isForceAtlas2Running()) { + sigma_utils.ourStopFA2(); + } - // register our renderers in sigma module - this.prepareSigmaCustomRenderers = function(sigmaModule) { - - // £TODO group the rendering primitives it all together - // and perhaps move here where the preparation occurs - var tempo = new SigmaUtils(); - + changeType(); + setTimeout(function() { + // $('.etabs a[href="#tagCloudXR"]').trigger('click'); + $('#selection-tabs-contnr').easytabs('select', '#tagcloud-XR'); + }, 500); + }); + + $('#changelevel').click(function() { + console.log('changeLEVEL click'); + if (TW.partialGraph.isForceAtlas2Running()) { + sigma_utils.ourStopFA2(); + } + changeLevel(); + }); + + // sidepanel folding + $('#sidefold').click(function() { + if (window.innerWidth >= 768) { + let width = TW.conf.sidePanelSize || '400px'; + // $("#ctlzoom").css('right','10px') + $('#ctlzoom').animate( + {'right': '10px'}, 'slow' + ); + $('#sidebar').animate( + {'right': `-${width}`}, 'slow', + function() { + $('#sigma-contnr').css('right', 0); + $('#sidebar').hide(); + TW.partialGraph.refresh(); + $('#sidefold').hide(); + $('#sideunfold').show(); + } + ); + TW.gui.foldedSide = true; + } + }); + + $('#sideunfold').click(function() { + if (window.innerWidth >= 768) { + let width = TW.conf.sidePanelSize || '400px'; + $('#sidebar').show(); + $('#sidebar').animate( + {'right': 0}, 'slow', + function() { + $('#sigma-contnr').css('right', width); + TW.partialGraph.refresh(); + $('#sideunfold').hide(); + $('#sidefold').show(); + $('#ctlzoom').css('right', `calc(${width} + 10px)`); + } + ); + TW.gui.foldedSide = false; + } + }); - // custom nodes rendering - // overriding the def is simplest - // (could also do it by type) - sigmaModule.canvas.nodes.def = tempo.twRender.canvas.nodes.withBorders + $('#tips').html(getTips()); - // custom edges rendering registered under 'curve' - sigmaModule.canvas.edges.curve = tempo.twRender.canvas.edges.curve - sigmaModule.canvas.edges.line = tempo.twRender.canvas.edges.line + // we start with no selection + $('#selection-tabs-contnr').hide(); - // custom labels rendering - // - based on the normal one sigma.canvas.labels.def - // - additionnaly supports 'active/highlight' node property (magnify x 3) - // - also handles 'forceLabel' property - sigmaModule.canvas.labels.def = tempo.twRender.canvas.labels.largeractive + // #saveAs => toggle #savemodal initialized in html + bootstrap-native - // custom hovers rendering - // - based on the normal one sigma.canvas.hovers.def - // - additionnaly magnifies all labels x 2 - // - additionnaly supports 'active/highlight' node property (magnify x 3) - sigmaModule.canvas.hovers.def = tempo.twRender.canvas.hovers.largerall + // button CENTER + $('#lensButton').click(function() { + // new sigma.js + TW.partialGraph.camera.goTo({x: 0, y: 0, ratio: 1.2}); + }); - if (TW.conf.debug.logSettings) console.log('tw renderers registered in sigma module') + if (!TW.conf.colorByAtt) { + $('#setcolorsMenu').hide(); } - this.initSearchListeners = function () { - - var selectionEngine = this.selNgn - - $('input#searchinput').autocomplete({ - source: function(request, response) { - // labels initialized in settings, filled in updateSearchLabels - // console.log(labels.length) - var matches = []; - var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i"); - // grep at heart - var results = $.grep(TW.labels, function(e) { - return matcher.test(e.label); //|| matcher.test(e.desc); - }); - - if (!results.length) { - $("#noresults").text("Pas de résultats"); - } else { - $("#noresults").empty(); - } - matches = results.slice(0, TW.conf.maxSuggestionsAutoComplete); - response(matches); - - }, - minLength: TW.conf.minLengthAutoComplete, - - - // ----------------------->8--------------------- - // send a "no more suggestions event" - response: function (event, ui_response_array) { - // exemple in ui_response_array - // {"content": - // [{id: 239, label:"warning system",desc:"ISIterms.."}, - // ... - // ] - // } - - if (ui_response_array.content.length == 0) { - // we send our "noAutocomplete" event - $('#searchinput').trigger("tw:noAutocomplete"); - // (signal for plugins like crowdsourcing that - // are sensitive to autocomplete being empty) - // console.log("000 Event [noAutocomplete] sent from Tinaweb search") - } - else { - // we send a "hasAutocomplete" event - // (/!\ will be sent for each typed char) - $('#searchinput').trigger("tw:gotAutocomplete"); - // console.log("+++ Event [gotAutocomplete] sent from Tinaweb search") - } - } - // ----------------------->8--------------------- - }); + if (TW.conf.fa2Available) { + $('#layoutButton').click(function() { + sigma_utils.smartForceAtlas({'manual': true}); + }); + } else { + $('#layoutButton').hide(); + } - // Search by click on the search button, independently from autocomplete - $("#searchbutton").click(function() { - var query = normalizeString($("#searchinput").val()) - // console.log('===\nyour query was: "'+query+'"'); - - // --- SelectionEngine.search() ------------------- - // -> will call sigmaUtils.find() - // over sigmaUtils.getnodesIndex() - // -> then call this.SelectorEngine - // and this.MultipleSelection2 - selectionEngine.search_n_select(query) - // ------------------------------------------------ - }); + if (TW.conf.disperseAvailable) { + $('#noverlapButton').click(function() { + if (! TW.partialGraph.isNoverlapRunning()) { + // show waiting cursor on page and button + TW.gui.elHtml.classList.add('waiting'); + this.style.cursor = 'wait'; + // and waiting icon + this.insertBefore(createWaitIcon('noverlapwait'), this.children[0]); + + // reconfigure to account for nodesizes (if current sizes up => margin needs up) + let sizeFactor = Math.max.apply(null, TW.gui.sizeRatios); + TW.gui.noverlapConf.nodeMargin = .5 * sizeFactor; + TW.gui.noverlapConf.scaleNodes = 1.5 * sizeFactor; + TW.partialGraph.configNoverlap(TW.gui.noverlapConf); + let listener = TW.partialGraph.startNoverlap(); + let noverButton = this; + listener.bind('stop', function(event) { + let stillRunning = document.getElementById('noverlapwait'); + if (stillRunning) { + reInitFa2({ + localZoneSettings: !TW.SystemState().level, + skipHidden: !TW.conf.stablePositions, + callback: function() { + console.debug('noverlap: updated fa2 positions'); + }, + }); - // Search by pressing ENTER, independently from autocomplete - $("#searchinput").keydown(function (e) { - if (e.keyCode == 13) { - var query = normalizeString($("#searchinput").val()) - selectionEngine.search_n_select(query) + TW.gui.elHtml.classList.remove('waiting'); + noverButton.style.cursor = 'auto'; + stillRunning.remove(); } - }); - } + }); - // to init handlers + elts for tina GUI environment (run once on page load) - this.initGUIListeners = function () { - - var body=document.getElementsByTagName('body')[0]; - body.style.paddingTop="41px"; - - - // side panel width - if(TW.conf.sidePanelSize && TW.conf.sidePanelSize != "400px") { - // change stylesheet rules preferably to element style directly - // (this way we don't block the mobile-variants CSS effects, - // b/c twjs-mobile.css is loaded after twjs.css in the html) - if (TW.gui.sheets) { - if (TW.gui.sheets.main) { - TW.gui.sheets.main.insertRule( - `#sidebar {width: ${TW.conf.sidePanelSize};}`, - TW.gui.sheets.main.cssRules.length - ) - TW.gui.sheets.main.insertRule( - `#sigma-contnr {right: ${TW.conf.sidePanelSize};}`, - TW.gui.sheets.main.cssRules.length - ) - } - if (TW.gui.sheets.panels) { - TW.gui.sheets.panels.insertRule( - `#ctlzoom {right: calc(${TW.conf.sidePanelSize} + 10px);}`, - TW.gui.sheets.panels.cssRules.length - ) - } - } - // otherwise we do it the easy way - else { - console.warn("Couldn't identify twjs.css and selection-panels.css") - document.getElementById('sidebar').style.width = TW.conf.sidePanelSize - document.getElementById('sigma-contnr').style.right = TW.conf.sidePanelSize - document.getElementById('ctlzoom').style.right = `calc(${TW.conf.sidePanelSize} + 10px)` - } + return; } + }); + } else { + $('#noverlapButton').hide(); + } - // tab handlers - $('.etabs').click(function(){ - setTimeout( - function() { - $("#read-opposite-neighs").readmore({maxHeight:200}); - $("#read-sameside-neighs").readmore({maxHeight:200}); - }, - 500 - ) - }); - - $("#changetype").click(function(){ - console.log("changeTYPE click"); - if (TW.partialGraph.isForceAtlas2Running()) - sigma_utils.ourStopFA2(); - - changeType(); - setTimeout(function(){ - // $('.etabs a[href="#tagCloudXR"]').trigger('click'); - $('#selection-tabs-contnr').easytabs('select', '#tagcloud-XR') - },500) - }); - - $("#changelevel").click(function(){ - console.log("changeLEVEL click"); - if (TW.partialGraph.isForceAtlas2Running()) - sigma_utils.ourStopFA2(); - changeLevel(); - }); - - // sidepanel folding - $('#sidefold').click(function(){ - if (window.innerWidth >= 768) { - let width = TW.conf.sidePanelSize || '400px' - // $("#ctlzoom").css('right','10px') - $("#ctlzoom").animate( - {"right": "10px"}, "slow" - ) - $("#sidebar").animate( - {"right":`-${width}`}, "slow", - function(){ - $("#sigma-contnr").css('right',0) - $("#sidebar").hide() - TW.partialGraph.refresh() - $("#sidefold").hide() - $("#sideunfold").show() - } - ); - TW.gui.foldedSide = true - } - }) - - $('#sideunfold').click(function(){ - if (window.innerWidth >= 768) { - let width = TW.conf.sidePanelSize || '400px' - $("#sidebar").show() - $("#sidebar").animate( - {"right": 0}, "slow", - function(){ - $("#sigma-contnr").css('right', width) - TW.partialGraph.refresh() - $("#sideunfold").hide() - $("#sidefold").show() - $("#ctlzoom").css('right',`calc(${width} + 10px)`) - } - ); - TW.gui.foldedSide = false - } - }) - - $("#tips").html(getTips()); - - // we start with no selection - $("#selection-tabs-contnr").hide(); - - // #saveAs => toggle #savemodal initialized in html + bootstrap-native - - // button CENTER - $("#lensButton").click(function () { - // new sigma.js - TW.partialGraph.camera.goTo({x:0, y:0, ratio:1.2}) - }); - - if (!TW.conf.colorByAtt) { - $("#setcolorsMenu").hide() - } - if (TW.conf.fa2Available) { - $("#layoutButton").click(function () { - sigma_utils.smartForceAtlas({'manual': true}) - }); + $('#edges-switch').click(function() { + sigma_utils.toggleEdges(this.checked); + }); + + + // Cursor Size slider + TW.gui.circleSlider = $('#unranged-value').freshslider({ + step: 1, + min: TW.conf.circleSizeMin, + max: TW.conf.circleSizeMax, + value: TW.gui.circleSize, + onchange: function(value) { + // console.log("en cursorsize: "+value); + TW.gui.circleSize = value; + }, + }); + + // double click on cursor selector slider => set it to 0 + $('#areacircle-size').dblclick(function() { + TW.gui.circleSlider.setValue(0); + }); + + // costly entire refresh (~400ms) only after stopped resizing for 3s + // NB: rescale middleware already reacted and, except for large win size changes, it handles the resize fine + // (so this fragment is only to accomodate the large changes) + let winResizeTimeout = null; + window.addEventListener('resize', function(ev) { + if (winResizeTimeout) { + clearTimeout(winResizeTimeout); + } + winResizeTimeout = setTimeout(function() { + if (window.TW.partialGraph && window.TW.partialGraph.refresh) { + window.TW.partialGraph.refresh(); + // console.log('did refresh') } - else { - $("#layoutButton").hide() + if (TW.gui.elHtml.classList) { + TW.gui.elHtml.classList.remove('waiting'); } - if (TW.conf.disperseAvailable) { - $("#noverlapButton").click(function () { - if(! TW.partialGraph.isNoverlapRunning()) { - // show waiting cursor on page and button - TW.gui.elHtml.classList.add('waiting'); - this.style.cursor = 'wait' - // and waiting icon - this.insertBefore(createWaitIcon('noverlapwait'), this.children[0]) - - // reconfigure to account for nodesizes (if current sizes up => margin needs up) - let sizeFactor = Math.max.apply(null, TW.gui.sizeRatios) - TW.gui.noverlapConf.nodeMargin = .5 * sizeFactor - TW.gui.noverlapConf.scaleNodes = 1.5 * sizeFactor - TW.partialGraph.configNoverlap(TW.gui.noverlapConf) - var listener = TW.partialGraph.startNoverlap(); - var noverButton = this - listener.bind('stop', function(event) { - var stillRunning = document.getElementById('noverlapwait') - if (stillRunning) { - reInitFa2({ - localZoneSettings: !TW.SystemState().level, - skipHidden: !TW.conf.stablePositions, - callback: function() {console.debug("noverlap: updated fa2 positions")} - }) - - TW.gui.elHtml.classList.remove('waiting'); - noverButton.style.cursor = 'auto' - stillRunning.remove() - } - }); - return; - } - }); + // monitor passing out of or into smaller width + // (along with twjs-mobile.css and selection-panels.mobile.css) + if (ev.target.innerWidth < 768 && !TW.gui.smallView) { + TW.gui.smallView = true; + cssReset(); + $('#sideunfold,#sidefold').hide(); + } else if (ev.target.innerWidth >= 768 && TW.gui.smallView) { + TW.gui.smallView = false; + foldingReset(); } - else { - $("#noverlapButton").hide() - } - - - $("#edges-switch").click(function () { - sigma_utils.toggleEdges(this.checked) - }); - - - //Cursor Size slider - TW.gui.circleSlider = $("#unranged-value").freshslider({ - step: 1, - min:TW.conf.circleSizeMin, - max:TW.conf.circleSizeMax, - value:TW.gui.circleSize, - onchange:function(value){ - // console.log("en cursorsize: "+value); - TW.gui.circleSize=value; - } - }); - - // double click on cursor selector slider => set it to 0 - $("#areacircle-size").dblclick(function(){ - TW.gui.circleSlider.setValue(0) - }); - - // costly entire refresh (~400ms) only after stopped resizing for 3s - // NB: rescale middleware already reacted and, except for large win size changes, it handles the resize fine - // (so this fragment is only to accomodate the large changes) - var winResizeTimeout = null - window.addEventListener('resize', function(ev){ - if (winResizeTimeout) { - clearTimeout(winResizeTimeout) - } - winResizeTimeout = setTimeout(function() { - - if (window.TW.partialGraph && window.TW.partialGraph.refresh) { - window.TW.partialGraph.refresh() - // console.log('did refresh') - } - if (TW.gui.elHtml.classList) { - TW.gui.elHtml.classList.remove('waiting'); - } - - - // monitor passing out of or into smaller width - // (along with twjs-mobile.css and selection-panels.mobile.css) - if (ev.target.innerWidth < 768 && !TW.gui.smallView) { - TW.gui.smallView = true - cssReset() - $('#sideunfold,#sidefold').hide() - } - else if (ev.target.innerWidth >= 768 && TW.gui.smallView) { - TW.gui.smallView = false - foldingReset() - } - - }, 1000) - }, true) + }, 1000); + }, true); - // general listener: shift key in the window <=> add to selection - $(document).on('keyup keydown', function(e){ - // changes the global boolean ("add node to selection" status) if keydown and SHIFT - TW.gui.checkBox = TW.gui.manuallyChecked || e.shiftKey + // general listener: shift key in the window <=> add to selection + $(document).on('keyup keydown', function(e) { + // changes the global boolean ("add node to selection" status) if keydown and SHIFT + TW.gui.checkBox = TW.gui.manuallyChecked || e.shiftKey; - // show it in the real TW.gui.checkBox too - $('#checkboxdiv').prop("checked", TW.gui.manuallyChecked || e.shiftKey) + // show it in the real TW.gui.checkBox too + $('#checkboxdiv').prop('checked', TW.gui.manuallyChecked || e.shiftKey); - // also listen for CTRL+Z 17 + 90 - if (e.type == "keyup" - && (e.which == 90 || e.keyCode == 90) && e.ctrlKey - && TW.states.length > 2 - ) { + // also listen for CTRL+Z 17 + 90 + if (e.type == 'keyup' + && (e.which == 90 || e.keyCode == 90) && e.ctrlKey + && TW.states.length > 2 + ) { + if (timeoutIdCTRLZ) { + window.clearTimeout(timeoutIdCTRLZ); + } - if (timeoutIdCTRLZ) { - window.clearTimeout(timeoutIdCTRLZ) + let timeoutIdCTRLZ = window.setTimeout(function() { + // console.log("pop state") + let previousState = TW.states.pop(); + + deselectNodes(previousState); + + let returningState = TW.SystemState(); + + // restoring level (will also restore selections) + if (returningState.level != previousState.level) { + changeLevel(returningState); + } else { + // restoring selection + if (returningState.selectionNids.length) { + TW.gui.selectionActive = true; + // changes active/highlight and refresh + // POSS turn the nostate version into a select fun like deselect (ie no state, no refresh) + TW.instance.selNgn.multipleSelection2({ + nodes: returningState.selectionNids, + noState: true, + }); + } else { + TW.gui.selectionActive = false; + TW.partialGraph.refresh(); } - - var timeoutIdCTRLZ = window.setTimeout(function() { - // console.log("pop state") - let previousState = TW.states.pop() - - deselectNodes(previousState) - - let returningState = TW.SystemState() - - // restoring level (will also restore selections) - if (returningState.level != previousState.level) { - changeLevel(returningState) - } - else { - // restoring selection - if (returningState.selectionNids.length) { - TW.gui.selectionActive = true - // changes active/highlight and refresh - // POSS turn the nostate version into a select fun like deselect (ie no state, no refresh) - TW.instance.selNgn.MultipleSelection2({ - nodes: returningState.selectionNids, - noState: true - }) - } - else { - TW.gui.selectionActive = false - TW.partialGraph.refresh() - } - } - }, 100) } - } ); - - } // finish envListeners - - - // to init local, instance-related listeners (need to run at new sigma instance) - // args: @partialGraph = a sigma instance - // accessed globals: TW.Facets - this.initSigmaListeners = function(partialGraph, initialActivetypes, initialActivereltypes, optionalRelDocsConf) { - - // console.log("initSigmaListeners TW.categories / types array / reltypeskeys array: ", TW.categories, initialActivetypes, initialActivereltypes) - - var selectionEngine = this.selNgn - - // changetype button - if (TW.categories.length == 1) { - $("#changetype").hide(); - } - else { - $("#changetype").show(); + }, 100); } + } ); + } + + + // to init local, instance-related listeners (need to run at new sigma instance) + // args: @partialGraph = a sigma instance + // accessed globals: TW.Facets + initSigmaListeners(partialGraph, initialActivetypes, initialActivereltypes, + optionalRelDocsConf) { + let selectionEngine = this.selNgn; + + // changetype button + if (TW.categories.length == 1) { + $('#changetype').hide(); + } else { + $('#changetype').show(); + } - // sigma events bindings - // --------------------- + // sigma events bindings + // --------------------- - // cf. https://github.com/jacomyal/sigma.js/wiki/Events-API + // cf. https://github.com/jacomyal/sigma.js/wiki/Events-API - // cases: - // 'click' - simple click, early event - // used for area (with global: TW.gui.circleSize) - // 'clickNode'- simple click, second event if one node + // cases: + // 'click' - simple click, early event + // used for area (with global: TW.gui.circleSize) + // 'clickNode'- simple click, second event if one node - // when circle area select - // ======================== - // 1st event, even before we know if there are nodes - partialGraph.bind('click', function(e) { - // console.log("sigma click event e", e) + // when circle area select + // ======================== + // 1st event, even before we know if there are nodes + partialGraph.bind('click', function(e) { + // console.log("sigma click event e", e) - // case with a selector circle cursor handled here - if (TW.gui.circleSize > 0) { - // actual click position, but in graph coords - var x = e.data.x - var y = e.data.y + // case with a selector circle cursor handled here + if (TW.gui.circleSize > 0) { + // actual click position, but in graph coords + let x = e.data.x; + let y = e.data.y; - // convert - var camCoords = TW.cam.cameraPosition(x,y) + // convert + let camCoords = TW.cam.cameraPosition(x, y); - // retrieve area nodes, using indexed quadtree and global TW.gui.circleSize - var circleNodes = circleGetAreaNodes( - camCoords.x, - camCoords.y - ) + // retrieve area nodes, using indexed quadtree and global TW.gui.circleSize + let circleNodes = circleGetAreaNodes( + camCoords.x, + camCoords.y + ); - selectionEngine.runAndEffects(circleNodes) - } - }) + selectionEngine.runAndEffects(circleNodes); + } + }); - // when one node and normal click - // =============================== - partialGraph.bind('clickNode', function(e) { - // console.log("clickNode event e", e) + // when one node and normal click + // =============================== + partialGraph.bind('clickNode', function(e) { + // console.log("clickNode event e", e) - // new sigma.js gives easy access to clicked node! - var theNodeId = e.data.node.id + // new sigma.js gives easy access to clicked node! + let theNodeId = e.data.node.id; - if (TW.gui.circleSize == 0) { - selectionEngine.runAndEffects([theNodeId]) - } - // case with a selector circle cursor handled - // just before, at click event - }) + if (TW.gui.circleSize == 0) { + selectionEngine.runAndEffects([theNodeId]); + } + // case with a selector circle cursor handled + // just before, at click event + }); - // doubleClick creates new meso view around clicked node - partialGraph.bind('doubleClickNode', function(e) { - // /!\ doubleClick will also fire 2 singleClick events /!\ - // - // https://github.com/jacomyal/sigma.js/issues/208 - // https://github.com/jacomyal/sigma.js/issues/506 - // - // (order: clickNode, doubleClickNode, clickNode) - // 1st 2nd (NOW) 3rd + // doubleClick creates new meso view around clicked node + partialGraph.bind('doubleClickNode', function(e) { + // /!\ doubleClick will also fire 2 singleClick events /!\ + // + // https://github.com/jacomyal/sigma.js/issues/208 + // https://github.com/jacomyal/sigma.js/issues/506 + // + // (order: clickNode, doubleClickNode, clickNode) + // 1st 2nd (NOW) 3rd - // so if this was also a new selection, the 1st clickNode did handle it - // => we just create the new zoom level + // so if this was also a new selection, the 1st clickNode did handle it + // => we just create the new zoom level - // A - create new zoom level state - TW.pushGUIState({ level: false }) - // NB2: we never switch back to macro level from doubleClick + // A - create new zoom level state + TW.pushGUIState({level: false}); + // NB2: we never switch back to macro level from doubleClick - // B - apply it without changing state - changeLevel(TW.SystemState()) - }) + // B - apply it without changing state + changeLevel(TW.SystemState()); + }); - // when click in the empty background - // ================================== - if (TW.conf.deselectOnclickStage) { - partialGraph.bind('clickStage', function(e) { - // console.log("clickStage event e", e) + // when click in the empty background + // ================================== + if (TW.conf.deselectOnclickStage) { + partialGraph.bind('clickStage', function(e) { + // console.log("clickStage event e", e) - if (! e.data.captor.isDragging + if (! e.data.captor.isDragging && TW.gui.selectionActive && ! TW.gui.circleSize) { + // we clear selections and all its effects + cancelSelection(false); + } + }); + } - // we clear selections and all its effects - cancelSelection(false); - } - }) - } + // for all TW.cam.goTo (move/zoom) events + // =============== + TW.cam.bind('coordinatesUpdated', function(e) { + $('#zoomSlider').slider('value', Math.log(1/(TW.cam.ratio+zoomSliRangeRatio))); + }); + + + // dragNodes plugin + if (TW.conf.dragNodesAvailable) { + let dragListener = sigma.plugins.dragNodes(partialGraph, partialGraph.renderers[0]); + + dragListener.bind('dragend', function(dragEndEvent) { + let mouseEvent = dragEndEvent.data.captor; + if (mouseEvent.ctrlKey) { + // update FA2 positions array + reInitFa2({ + callback: function() { +console.debug('dragNodes: updated fa2 positions'); +}, + }); + } + }); - // for all TW.cam.goTo (move/zoom) events - // =============== - var zoomTimeoutId = null - TW.cam.bind('coordinatesUpdated', function(e) { - $("#zoomSlider").slider("value",Math.log(1/(TW.cam.ratio+zoomSliRangeRatio))) - }) - - - // dragNodes plugin - if (TW.conf.dragNodesAvailable) { - var dragListener = sigma.plugins.dragNodes(partialGraph, partialGraph.renderers[0]); - - dragListener.bind("dragend", function(dragEndEvent){ - let mouseEvent = dragEndEvent.data.captor - if (mouseEvent.ctrlKey) { - // update FA2 positions array - reInitFa2({ - callback: function() {console.debug("dragNodes: updated fa2 positions")} - }) - } - }) - - // intercept dragNodes events if not CTRL+click - document.getElementById('sigma-contnr').addEventListener( - 'mousemove', - function(ev) { - if (!ev.ctrlKey) { - ev.stopPropagation() - } + // intercept dragNodes events if not CTRL+click + document.getElementById('sigma-contnr').addEventListener( + 'mousemove', + function(ev) { + if (!ev.ctrlKey) { + ev.stopPropagation(); } - ) - } - - // --------------------------------------------------------------------- + } + ); + } - // POSS: bind to captors (0=>mouse, 1=>touch) - // TW.rend.captors[0].bind('mousemove', function(e) { - // console.log("mousemove event e", e.data.node) - // - // }) + // --------------------------------------------------------------------- - // --------------------------------------------------------------------- + // POSS: bind to captors (0=>mouse, 1=>touch) + // TW.rend.captors[0].bind('mousemove', function(e) { + // console.log("mousemove event e", e.data.node) + // + // }) - // raw events (non-sigma): handlers attached to the container - // ========== - $("#sigma-contnr") - .mousemove(function(e){ - if(!isUndef(partialGraph)) { - // show/move selector circle cursor - if(TW.gui.circleSize>0) circleTrackMouse(e); - } - }) - - // POSSible for the future: add tools to contextmenu - // .contextmenu(function(){ - // return false; - // }) - - // sliders events - // ============== - - var zoomSliRangeRatio = TW.conf.zoomMin/TW.conf.zoomMax - var zoomSliBoundaryTop = Math.log(1/(TW.conf.zoomMin+zoomSliRangeRatio)) - var zoomSliBoundaryBot = Math.log(1/(TW.conf.zoomMax+zoomSliRangeRatio)) - var zoomSliOrigin = Math.log(1/(TW.cam.ratio+zoomSliRangeRatio)) - var zoomSliStep = (zoomSliBoundaryTop - zoomSliBoundaryBot)/50 - - $("#zoomSlider").slider({ - orientation: "vertical", - - // new sigma.js current zoom ratio - value: partialGraph.camera.ratio, - min: zoomSliBoundaryBot, // ex log(1/(ZOOM-OUT_RATIO+k)) - max: zoomSliBoundaryTop, // ex log(1/(ZOOM-IN_RATIO+k)) - // where k is the ratio of the full range - - // range: true, - step: zoomSliStep, - value: zoomSliOrigin, - slide: function( event, ui ) { - TW.partialGraph.camera.goTo({ - // we use 1/e^x -k transform for result like logscale on 1/x - ratio: 1/Math.exp(ui.value) - zoomSliRangeRatio - }); - } - }); + // --------------------------------------------------------------------- - $("#zoomPlusButton").click(function () { - var newRatio = TW.cam.ratio * .75 - if (newRatio >= TW.conf.zoomMin) { - // triggers coordinatesUpdated which sets the slider cursor - partialGraph.camera.goTo({ratio: newRatio}); - return false; - } - }); - - $("#zoomMinusButton").click(function () { - var newRatio = TW.cam.ratio * 1.25 - if (newRatio <= TW.conf.zoomMax) { - // triggers coordinatesUpdated which sets the slider cursor - partialGraph.camera.goTo({ratio: newRatio}); - return false; + // raw events (non-sigma): handlers attached to the container + // ========== + $('#sigma-contnr') + .mousemove(function(e) { + if (!isUndef(partialGraph)) { + // show/move selector circle cursor + if (TW.gui.circleSize>0) circleTrackMouse(e); } }); - // initialize selection tabs (order: show => easytabs => hide => readmore) - if (TW.categories.length == 1) { - $('#selection-tabs-contnr').easytabs({ - updateHash:false, - defaultTab: 'li#tabsameside' + // POSSible for the future: add tools to contextmenu + // .contextmenu(function(){ + // return false; + // }) + + // sliders events + // ============== + + let zoomSliRangeRatio = TW.conf.zoomMin/TW.conf.zoomMax; + let zoomSliBoundaryTop = Math.log(1/(TW.conf.zoomMin+zoomSliRangeRatio)); + let zoomSliBoundaryBot = Math.log(1/(TW.conf.zoomMax+zoomSliRangeRatio)); + let zoomSliOrigin = Math.log(1/(TW.cam.ratio+zoomSliRangeRatio)); + let zoomSliStep = (zoomSliBoundaryTop - zoomSliBoundaryBot)/50; + + $('#zoomSlider').slider({ + orientation: 'vertical', + + // new sigma.js current zoom ratio + value: partialGraph.camera.ratio, + min: zoomSliBoundaryBot, // ex log(1/(ZOOM-OUT_RATIO+k)) + max: zoomSliBoundaryTop, // ex log(1/(ZOOM-IN_RATIO+k)) + // where k is the ratio of the full range + + // range: true, + step: zoomSliStep, + value: zoomSliOrigin, + slide: function(event, ui) { + TW.partialGraph.camera.goTo({ + // we use 1/e^x -k transform for result like logscale on 1/x + ratio: 1/Math.exp(ui.value) - zoomSliRangeRatio, }); - $("#taboppos").hide(); - $("#read-sameside-neighs").readmore({maxHeight:200}); + }, + }); + + $('#zoomPlusButton').click(function() { + let newRatio = TW.cam.ratio * .75; + if (newRatio >= TW.conf.zoomMin) { + // triggers coordinatesUpdated which sets the slider cursor + partialGraph.camera.goTo({ratio: newRatio}); + return false; } - else { - $("#taboppos").show(); - $('#selection-tabs-contnr').easytabs({ - updateHash:false, - defaultTab: 'li#taboppos' - }); - $("#read-sameside-neighs").readmore({maxHeight:200}); - $("#read-opposite-neighs").readmore({maxHeight:200}); + }); + + $('#zoomMinusButton').click(function() { + let newRatio = TW.cam.ratio * 1.25; + if (newRatio <= TW.conf.zoomMax) { + // triggers coordinatesUpdated which sets the slider cursor + partialGraph.camera.goTo({ratio: newRatio}); + return false; } + }); - // initialize reldocs tabs if declared in optionalRelDocsConf - // (optionalRelDocsConf function-scope name of TW.currentRelDocsDBs) - if (TW.conf.getRelatedDocs && optionalRelDocsConf) { - resetTabs(initialActivetypes, optionalRelDocsConf) - } + // initialize selection tabs (order: show => easytabs => hide => readmore) + if (TW.categories.length == 1) { + $('#selection-tabs-contnr').easytabs({ + updateHash: false, + defaultTab: 'li#tabsameside', + }); + $('#taboppos').hide(); + $('#read-sameside-neighs').readmore({maxHeight: 200}); + } else { + $('#taboppos').show(); + $('#selection-tabs-contnr').easytabs({ + updateHash: false, + defaultTab: 'li#taboppos', + }); + $('#read-sameside-neighs').readmore({maxHeight: 200}); + $('#read-opposite-neighs').readmore({maxHeight: 200}); + } - // defaultColoring: an attribute name to immediately apply color with - let madeDefaultColor = false - if (TW.conf.defaultColoring) { - let colMethodName, colMethod - if (TW.facetOptions[TW.conf.defaultColoring]) { - colMethodName = TW.gui.colorFuns[TW.facetOptions[TW.conf.defaultColoring]['col']] - } - if (! colMethodName) { - if(TW.conf.defaultColoring.indexOf("clust")>-1||TW.conf.defaultColoring.indexOf("class")>-1) { - // for classes and clusters - colMethod = "clusterColoring" - } - else { - colMethod = "heatmapColoring" - } + // initialize reldocs tabs if declared in optionalRelDocsConf + // (optionalRelDocsConf function-scope name of TW.currentRelDocsDBs) + if (TW.conf.getRelatedDocs && optionalRelDocsConf) { + resetTabs(initialActivetypes, optionalRelDocsConf); + } + + // defaultColoring: an attribute name to immediately apply color with + let madeDefaultColor = false; + if (TW.conf.defaultColoring) { + let colMethodName; + let colMethod; + if (TW.facetOptions[TW.conf.defaultColoring]) { + colMethodName = TW.gui.colorFuns[TW.facetOptions[TW.conf.defaultColoring]['col']]; + } + if (! colMethodName) { + if (TW.conf.defaultColoring.indexOf('clust')>-1||TW.conf.defaultColoring.indexOf('class')>-1) { + // for classes and clusters + colMethod = 'clusterColoring'; + } else { + colMethod = 'heatmapColoring'; } + } - // retrieve the actual function and if there, try and run it - colMethod = window[colMethodName] - if (colMethod && typeof colMethod == "function") { - try { - colMethod(TW.conf.defaultColoring) - madeDefaultColor = true - } - catch(err) { - console.warn(`Settings asked for defaultColoring by the + // retrieve the actual function and if there, try and run it + colMethod = window[colMethodName]; + if (colMethod && typeof colMethod == 'function') { + try { + colMethod(TW.conf.defaultColoring); + madeDefaultColor = true; + } catch (err) { + console.warn(`Settings asked for defaultColoring by the attribute "${TW.conf.defaultColoring}" but - it's not present in the dataset => skip action`) - } + it's not present in the dataset => skip action`); } } - // otherwise, set the default legend - if (! madeDefaultColor) { - updateColorsLegend ( "clust_default" ) - } + } + // otherwise, set the default legend + if (! madeDefaultColor) { + updateColorsLegend('clust_default'); + } - // select currently active sliders - if (TW.conf.filterSliders) { - // also for all active cats - for (let activeId in initialActivetypes) { - if (initialActivetypes[activeId]) { - // args: for display: target div , - // for context: family/type prop value, - // for values: the property to filter - NodeWeightFilter (`#slidercat${activeId}nodesweight` , activeId); - EdgeWeightFilter(`#slidercat${activeId}edgesweight`, - activeId.toString().repeat(2), - "weight" - ); - $(`.for-nodecategory-${activeId}`).show() - } - else { - $(`.for-nodecategory-${activeId}`).hide() - } + // select currently active sliders + if (TW.conf.filterSliders) { + // also for all active cats + for (let activeId in initialActivetypes) { + if (initialActivetypes[activeId]) { + // args: for display: target div , + // for context: family/type prop value, + // for values: the property to filter + NodeWeightFilter(`#slidercat${activeId}nodesweight`, activeId); + EdgeWeightFilter(`#slidercat${activeId}edgesweight`, + activeId.toString().repeat(2), + 'weight' + ); + $(`.for-nodecategory-${activeId}`).show(); + } else { + $(`.for-nodecategory-${activeId}`).hide(); } } + } - // node's label size - var labelSizeTimeout = null - $("#sliderlabelsize0").freshslider({ - step:.25, - min:.25, - max:5, - value: 1, - bgcolor:"#FFA500", - onchange:function(value){ - if (labelSizeTimeout) { - clearTimeout(labelSizeTimeout) - } - labelSizeTimeout = setTimeout(function(){ - if (TW.gui.sizeRatios[0] != value) { - TW.gui.sizeRatios[0] = value - // also adapt label threshold - TW.partialGraph.settings('labelThreshold', getSizeFactor()) - TW.partialGraph.render() - } - }, 200) - } - }); - var labelSizeTimeout = null - $("#sliderlabelsize1").freshslider({ - step:.25, - min:.25, - max:5, - value: 1, - bgcolor:"#27c470", - onchange:function(value){ - if (labelSizeTimeout) { - clearTimeout(labelSizeTimeout) - } - labelSizeTimeout = setTimeout(function(){ - if (TW.gui.sizeRatios[1] != value) { - TW.gui.sizeRatios[1] = value - // also adapt label threshold - TW.partialGraph.settings('labelThreshold', getSizeFactor()) - TW.partialGraph.render() - } - }, 200) + // node's label size + let labelSizeTimeout = null; + $('#sliderlabelsize0').freshslider({ + step: .25, + min: .25, + max: 5, + value: 1, + bgcolor: '#FFA500', + onchange: function(value) { + if (labelSizeTimeout) { + clearTimeout(labelSizeTimeout); + } + labelSizeTimeout = setTimeout(function() { + if (TW.gui.sizeRatios[0] != value) { + TW.gui.sizeRatios[0] = value; + // also adapt label threshold + TW.partialGraph.settings('labelThreshold', getSizeFactor()); + TW.partialGraph.render(); } - }); - - // set the switch - document.getElementById('edges-switch').checked = TW.customSettings.drawEdges - - // hide GUI elements of inactive types - // (frontend currently allows max 2 types) - for (var possibleTypeid in [0,1]) { - if ( ! TW.categories[possibleTypeid] - || ! initialActivetypes[possibleTypeid]) { - $(".for-nodecategory-"+possibleTypeid).hide(); + }, 200); + }, + }); + + $('#sliderlabelsize1').freshslider({ + step: .25, + min: .25, + max: 5, + value: 1, + bgcolor: '#27c470', + onchange: function(value) { + if (labelSizeTimeout) { + clearTimeout(labelSizeTimeout); } + labelSizeTimeout = setTimeout(function() { + if (TW.gui.sizeRatios[1] != value) { + TW.gui.sizeRatios[1] = value; + // also adapt label threshold + TW.partialGraph.settings('labelThreshold', getSizeFactor()); + TW.partialGraph.render(); + } + }, 200); + }, + }); + + // set the switch + document.getElementById('edges-switch').checked = TW.customSettings.drawEdges; + + // hide GUI elements of inactive types + // (frontend currently allows max 2 types) + for (let possibleTypeid in [0, 1]) { + if (!TW.categories[possibleTypeid] + || !initialActivetypes[possibleTypeid]) { + $('.for-nodecategory-'+possibleTypeid).hide(); } + } - // attributes' facet-options init & handler - fillAttrsInForm('choose-attr') - document.getElementById('choose-attr').onchange = showAttrConf + // attributes' facet-options init & handler + fillAttrsInForm('choose-attr'); + document.getElementById('choose-attr').onchange = showAttrConf; - // add all numeric attributes to titlingMetric with option type fromFacets - fillAttrsInForm('attr-titling-metric', 'num') + // add all numeric attributes to titlingMetric with option type fromFacets + fillAttrsInForm('attr-titling-metric', 'num'); - // cancelSelection(false); - } + // cancelSelection(false); + } + /** + * Clears the graph instance. + */ + clearSigma() { + if (TW.partialGraph && TW.partialGraph.graph) { + TW.partialGraph.graph.clear(); + TW.partialGraph.refresh(); - // clears the graph instance - this.clearSigma = function() { - if (TW.partialGraph && TW.partialGraph.graph) { - TW.partialGraph.graph.clear() - TW.partialGraph.refresh() - - TW.pushGUIState({'sels':[]}) - TW.SystemState().selectionNids = [] + TW.pushGUIState({'sels': []}); + TW.SystemState().selectionNids = []; + } + } + + // our current choice: show only the last cat + // except when setting TW.conf.debug.initialShowAll + initialActivetypes(categories) { + let firstActivetypes = []; + for (let i = 0; i < categories.length; i++) { + if (TW.conf.debug.initialShowAll || i == categories.length-1) { + firstActivetypes.push(true); + } else { + firstActivetypes.push(false); } } - - - // our current choice: show only the last cat - // except when setting TW.conf.debug.initialShowAll - this.initialActivetypes = function( categories ) { - let firstActivetypes = [] - for(var i=0; i all reltypes + if (nodeActivetypes.indexOf(false) == -1) { + if (TW.categories.length == 1) { + activereltypes = ['00']; + } else if (TW.categories.length == 2) { + activereltypes = ['00', '11', 'XR']; + } + // POSSible: generalize if length > 1: recurse to generate all true/false combinations except the all-false one + } else { // normal case: one activereltype, equal to the initialActivetype key + activereltypes = [nodeActivetypes.indexOf(true).toString().repeat(2)]; } - // new business logic associating some activetypes to some activerels - // (it now allows multiple "relation-families" to be added as visible edges) - this.inferActivereltypes = function( nodeActivetypes ) { - let activereltypes = [] - // multiple nodetypes all true => all reltypes - if (nodeActivetypes.indexOf(false) == -1) { - if (TW.categories.length == 1) { - activereltypes = ['00'] - } - else if (TW.categories.length == 2) { - activereltypes = ['00', '11', 'XR'] - } - // POSSible: generalize if length > 1: recurse to generate all true/false combinations except the all-false one - } - // normal case: one activereltype, equal to the initialActivetype key - else { - activereltypes = [nodeActivetypes.indexOf(true).toString().repeat(2)] - } - - return activereltypes; - } + return activereltypes; + } - // POSS for one type => many (jutsu case) results are also interesting when - // "disconnecting" previous direct neighbors (making them indirect via XR) - // ie: - // 00 => [11, XR] - // 11 => [00, XR] -}; + // POSS for one type => many (jutsu case) results are also interesting when + // "disconnecting" previous direct neighbors (making them indirect via XR) + // ie: + // 00 => [11, XR] + // 11 => [00, XR] +} diff --git a/twmain/enviroment.js b/twmain/enviroment.js index 02026c5..4a11769 100755 --- a/twmain/enviroment.js +++ b/twmain/enviroment.js @@ -1,42 +1,41 @@ -'use strict;' +'use strict;'; // GUI commodity pointers -TW.gui = {} -TW.gui.elHtml = document.getElementsByTagName('html')[0] -TW.gui.elContainer = document.getElementById('sigma-contnr') -TW.gui.sheets = {} -for (var i in document.styleSheets) { +TW.gui = {}; +TW.gui.elHtml = document.getElementsByTagName('html')[0]; +TW.gui.elContainer = document.getElementById('sigma-contnr'); +TW.gui.sheets = {}; +for (let i in document.styleSheets) { if (/twjs.css$/.test(document.styleSheets[i].href)) { - TW.gui.sheets.main = document.styleSheets[i] - } - else if (/selection-panels.css$/.test(document.styleSheets[i].href)) { - TW.gui.sheets.panels = document.styleSheets[i] + TW.gui.sheets.main = document.styleSheets[i]; + } else if (/selection-panels.css$/.test(document.styleSheets[i].href)) { + TW.gui.sheets.panels = document.styleSheets[i]; } } // GUI vars -TW.gui.selectionActive = false // <== changes rendering mode -TW.gui.smallView=false; // tracks if small width +TW.gui.selectionActive = false; // <== changes rendering mode +TW.gui.smallView=false; // tracks if small width TW.gui.circleSize = 0; -TW.gui.circleSlider = null +TW.gui.circleSlider = null; TW.gui.checkBox=false; TW.gui.shiftKey=false; TW.gui.foldedSide=false; TW.gui.manuallyChecked = false; -TW.gui.lastFilters = {} // <= last values, by slider id -TW.gui.reldocTabs = [{}, {}] // <= by nodetype and then dbtype - -TW.gui.sizeRatios = [1,1] // sizeRatios per nodetype -TW.gui.handpickedcolors = {}; // <= changes rendering, by nodetype -TW.gui.handpickedcolorsReset = function (forTypes = TW.categories) { - TW.gui.handpickedcolors = {} - for (var k in forTypes) { +TW.gui.lastFilters = {}; // <= last values, by slider id +TW.gui.reldocTabs = [{}, {}]; // <= by nodetype and then dbtype + +TW.gui.sizeRatios = [1, 1]; // sizeRatios per nodetype +TW.gui.handpickedcolors = {}; // <= changes rendering, by nodetype +TW.gui.handpickedcolorsReset = function(forTypes = TW.categories) { + TW.gui.handpickedcolors = {}; + for (let k in forTypes) { TW.gui.handpickedcolors[forTypes[k]] = { 'alton': false, - 'altattr': null - } + 'altattr': null, + }; } -} +}; TW.gui.noverlapConf = { nodeMargin: .4, @@ -45,15 +44,15 @@ TW.gui.noverlapConf = { speed: 7, maxIterations: 8, easing: 'quadraticOut', // animation transition function - duration: 1500 // animation duration + duration: 1500, // animation duration // NB animation happens *after* processing -} +}; TW.FA2Params = { // adapting speed ------------- slowDown: 1.5, - startingIterations: 2, // keep it an even number to reduce visible oscillations at rendering - iterationsPerRender: 4, // idem + startingIterations: 2, // keep it an even number to reduce visible oscillations at rendering + iterationsPerRender: 4, // idem barnesHutOptimize: false, // barnesHutTheta: .5, @@ -64,67 +63,67 @@ TW.FA2Params = { strongGravityMode: false, scalingRatio: 1, - adjustSizes: false, // ~ messy but sort of in favor of overlap prevention + adjustSizes: false, // ~ messy but sort of in favor of overlap prevention // favors global centrality // (but rather not needed when data already shows topic-centered // node groups and/nor when preferential attachment type of data) - outboundAttractionDistribution: false -} + outboundAttractionDistribution: false, +}; // POSS: themed variants (ex: for dark bg vs light bg) // contrasted color list for clusterColoring() -TW.gui.colorList = ["#000000", "#FFFF00", "#1CE6FF", "#FF34FF", "#FF4A46", "#008941", - "#006FA6", "#A30059", "#FFDBE5", "#7A4900", "#0000A6", "#63FFAC", "#B79762", - "#004D43", "#8FB0FF", "#997D87", "#5A0007", "#809693", "#FEFFE6", "#1B4400", - "#4FC601", "#3B5DFF", "#4A3B53", "#FF2F80", "#61615A", "#BA0900", "#6B7900", - "#00C2A0", "#FFAA92", "#FF90C9", "#B903AA", "#D16100", "#DDEFFF", "#000035", - "#7B4F4B", "#A1C299", "#300018", "#0AA6D8", "#013349", "#00846F", "#372101", - "#FFB500", "#C2FFED", "#A079BF", "#CC0744", "#C0B9B2", "#C2FF99", "#001E09", - "#00489C", "#6F0062", "#0CBD66", "#EEC3FF", "#456D75", "#B77B68", "#7A87A1", - "#788D66", "#885578", "#FAD09F", "#FF8A9A", "#D157A0", "#BEC459", "#456648", - "#0086ED", "#886F4C","#34362D", "#B4A8BD", "#00A6AA", "#452C2C", "#636375", - "#A3C8C9", "#FF913F", "#938A81", "#575329", "#00FECF", "#B05B6F", "#8CD0FF", - "#3B9700", "#04F757", "#C8A1A1", "#1E6E00", "#7900D7", "#A77500", "#6367A9", - "#A05837", "#6B002C", "#772600", "#D790FF", "#9B9700", "#549E79", "#FFF69F", - "#201625", "#72418F", "#BC23FF", "#99ADC0", "#3A2465", "#922329", "#5B4534", - "#FDE8DC", "#404E55", "#0089A3", "#CB7E98", "#A4E804", "#324E72", "#6A3A4C", - "#83AB58", "#001C1E", "#D1F7CE", "#004B28", "#C8D0F6", "#A3A489", "#806C66", - "#222800", "#BF5650", "#E83000", "#66796D", "#DA007C", "#FF1A59", "#8ADBB4", - "#1E0200", "#5B4E51", "#C895C5", "#320033", "#FF6832", "#66E1D3", "#CFCDAC", - "#D0AC94", "#7ED379", "#012C58"]; +TW.gui.colorList = ['#000000', '#FFFF00', '#1CE6FF', '#FF34FF', '#FF4A46', '#008941', + '#006FA6', '#A30059', '#FFDBE5', '#7A4900', '#0000A6', '#63FFAC', '#B79762', + '#004D43', '#8FB0FF', '#997D87', '#5A0007', '#809693', '#FEFFE6', '#1B4400', + '#4FC601', '#3B5DFF', '#4A3B53', '#FF2F80', '#61615A', '#BA0900', '#6B7900', + '#00C2A0', '#FFAA92', '#FF90C9', '#B903AA', '#D16100', '#DDEFFF', '#000035', + '#7B4F4B', '#A1C299', '#300018', '#0AA6D8', '#013349', '#00846F', '#372101', + '#FFB500', '#C2FFED', '#A079BF', '#CC0744', '#C0B9B2', '#C2FF99', '#001E09', + '#00489C', '#6F0062', '#0CBD66', '#EEC3FF', '#456D75', '#B77B68', '#7A87A1', + '#788D66', '#885578', '#FAD09F', '#FF8A9A', '#D157A0', '#BEC459', '#456648', + '#0086ED', '#886F4C', '#34362D', '#B4A8BD', '#00A6AA', '#452C2C', '#636375', + '#A3C8C9', '#FF913F', '#938A81', '#575329', '#00FECF', '#B05B6F', '#8CD0FF', + '#3B9700', '#04F757', '#C8A1A1', '#1E6E00', '#7900D7', '#A77500', '#6367A9', + '#A05837', '#6B002C', '#772600', '#D790FF', '#9B9700', '#549E79', '#FFF69F', + '#201625', '#72418F', '#BC23FF', '#99ADC0', '#3A2465', '#922329', '#5B4534', + '#FDE8DC', '#404E55', '#0089A3', '#CB7E98', '#A4E804', '#324E72', '#6A3A4C', + '#83AB58', '#001C1E', '#D1F7CE', '#004B28', '#C8D0F6', '#A3A489', '#806C66', + '#222800', '#BF5650', '#E83000', '#66796D', '#DA007C', '#FF1A59', '#8ADBB4', + '#1E0200', '#5B4E51', '#C895C5', '#320033', '#FF6832', '#66E1D3', '#CFCDAC', + '#D0AC94', '#7ED379', '#012C58']; // 24 colors + White, divided in cold and warm range for getHeatmapColors() fun -TW.gui.heatmapColorListWhite = "#F9F7ED" +TW.gui.heatmapColorListWhite = '#F9F7ED'; TW.gui.heatmapColorListToColdest = [ - "#B4FF50", - "#A4FF24", - "#79FF23", - "#42F923", - "#22F226", - "#02CB36", - "#01C462", - "#01BC8D", - "#00B5B1", - "#0088AE", - "#005197", - "#002FA0" -] + '#B4FF50', + '#A4FF24', + '#79FF23', + '#42F923', + '#22F226', + '#02CB36', + '#01C462', + '#01BC8D', + '#00B5B1', + '#0088AE', + '#005197', + '#002FA0', +]; TW.gui.heatmapColorListToWarmest = [ - "#FFE37A", - "#FFE008", - "#F0C508", - "#E89A09", - "#E48509", - "#DF7009", - "#DB5B09", - "#D7450A", - "#D3300A", - "#CF1B0A", - "#CB060B", - "#B21014" + '#FFE37A', + '#FFE008', + '#F0C508', + '#E89A09', + '#E48509', + '#DF7009', + '#DB5B09', + '#D7450A', + '#D3300A', + '#CF1B0A', + '#CB060B', + '#B21014', ]; @@ -134,108 +133,105 @@ TW.gui.heatmapColorListToWarmest = [ // then the 2 middle categories // will get the white "epsilon" color function getHeatmapColors(nClasses) { - // our dev param == colorListToWarmest.length == colorListToColdest.length - let listsLen = 12 + let listsLen = 12; // our result - var outColors = [] + let outColors = []; if (nClasses > listsLen*2) { - throw(`this function implementation can only give up to ${listsLen*2} classes`) + throw (`this function implementation can only give up to ${listsLen*2} classes`); } - let nHalfToPick = 0 + let nHalfToPick = 0; if (nClasses % 2 == 0) { - nHalfToPick = nClasses/2 - 1 - } - else { - nHalfToPick = (nClasses-1)/2 + nHalfToPick = nClasses/2 - 1; + } else { + nHalfToPick = (nClasses-1)/2; } // floor - let exactStep = listsLen / nHalfToPick - let skipStep = parseInt(exactStep) // incrmt must be int (used for arr idx) + let exactStep = listsLen / nHalfToPick; + let skipStep = parseInt(exactStep); // incrmt must be int (used for arr idx) // to compensate for the fractional part - let delta = exactStep - skipStep - let drift = 0 + let delta = exactStep - skipStep; + let drift = 0; // cold colors, starting from deepest color - for (let i = listsLen-1 ; i > 0 ; i -= skipStep ) { - outColors.push(TW.gui.heatmapColorListToColdest[i]) + for (let i = listsLen-1; i > 0; i -= skipStep ) { + outColors.push(TW.gui.heatmapColorListToColdest[i]); // catching back one step from time to time - drift += delta + drift += delta; if (drift >= 1) { - i-- - drift -= 1 + i--; + drift -= 1; } } // white - outColors.push(TW.gui.heatmapColorListWhite) + outColors.push(TW.gui.heatmapColorListWhite); if (nClasses % 2 == 0) { - outColors.push(TW.gui.heatmapColorListWhite) + outColors.push(TW.gui.heatmapColorListWhite); } // warm colors - for (let i = 0 ; i < listsLen ; i += skipStep ) { - outColors.push(TW.gui.heatmapColorListToWarmest[i]) + for (let i = 0; i < listsLen; i += skipStep ) { + outColors.push(TW.gui.heatmapColorListToWarmest[i]); } - return outColors + return outColors; } -function writeBrand (brandString, brandLink) { - let elTitle = document.getElementById('twbrand') +function writeBrand(brandString, brandLink) { + let elTitle = document.getElementById('twbrand'); if (elTitle) { - elTitle.innerHTML = brandString + elTitle.innerHTML = brandString; } - let anchors = document.getElementsByClassName('twbrand-link') - for (var k in anchors) { + let anchors = document.getElementsByClassName('twbrand-link'); + for (let k in anchors) { if (anchors[k] && anchors[k].href) { - anchors[k].href = brandLink + anchors[k].href = brandLink; } } } -function writeLabel (aMapLabel) { - document.getElementById('maplabel').innerHTML = aMapLabel +function writeLabel(aMapLabel) { + document.getElementById('maplabel').innerHTML = aMapLabel; } // some actions handled by js overrides the CSS from our stylesheets // => this function removes all these changes to restore stylesheet rules function cssReset() { - $("#sigma-contnr").css('right', '') - $("#ctlzoom").css('right','') - $("#sidebar").css('right','') - $("#sidebar").show() - TW.gui.foldedSide = false + $('#sigma-contnr').css('right', ''); + $('#ctlzoom').css('right', ''); + $('#sidebar').css('right', ''); + $('#sidebar').show(); + TW.gui.foldedSide = false; } function foldingReset() { if (TW.gui.foldedSide) { - $("#sideunfold").show() - $("#sidefold").hide() - } - else { - $("#sideunfold").hide() - $("#sidefold").show() + $('#sideunfold').show(); + $('#sidefold').hide(); + } else { + $('#sideunfold').hide(); + $('#sidefold').show(); } } -function alertCheckBox(eventCheck){ +function alertCheckBox(eventCheck) { // NB: we use 2 booleans to adapt to SHIFT checking // - var TW.gui.checkBox ---------> has the real box state // - var TW.gui.manuallyChecked --> remembers if it was changed here - if(!isUndef(eventCheck.checked)) { + if (!isUndef(eventCheck.checked)) { TW.gui.checkBox=eventCheck.checked; - TW.gui.manuallyChecked = eventCheck.checked + TW.gui.manuallyChecked = eventCheck.checked; } } @@ -243,74 +239,68 @@ function alertCheckBox(eventCheck){ // fileChooser: added to the environment when user opens explorer as local file // ----------- // TODO: because source files now get a project_conf.md, find a way to open it too if it exists -function createFilechooserEl () { - - var inputComment = document.createElement("p") - inputComment.innerHTML = `Choose a graph from your filesystem (gexf or json).` - inputComment.classList.add('comment') - inputComment.classList.add('centered') - - var graphFileInput = document.createElement('input') - graphFileInput.id = 'localgraphfile' - graphFileInput.type = 'file' - graphFileInput.accept = 'application/xml,application/gexf,application/json' - graphFileInput.classList.add('centered') +function createFilechooserEl() { + let inputComment = document.createElement('p'); + inputComment.innerHTML = `Choose a graph from your filesystem (gexf or json).`; + inputComment.classList.add('comment'); + inputComment.classList.add('centered'); + + let graphFileInput = document.createElement('input'); + graphFileInput.id = 'localgraphfile'; + graphFileInput.type = 'file'; + graphFileInput.accept = 'application/xml,application/gexf,application/json'; + graphFileInput.classList.add('centered'); // NB file input will trigger mainStartGraph() when the user chooses something graphFileInput.onchange = function() { if (this.files && this.files[0]) { - - let clientLocalGraphFile = this.files[0] + let clientLocalGraphFile = this.files[0]; // determine the format - let theFormat + let theFormat; if (/\.(?:gexf|xml)$/.test(clientLocalGraphFile.name)) { - theFormat = 'gexf' - } - else if (/\.json$/.test(clientLocalGraphFile.name)) { - theFormat = 'json' - } - else { - alert('unrecognized file format') + theFormat = 'gexf'; + } else if (/\.json$/.test(clientLocalGraphFile.name)) { + theFormat = 'json'; + } else { + alert('unrecognized file format'); } // retrieving the content - let rdr = new FileReader() + let rdr = new FileReader(); rdr.onload = function() { - if (! rdr.result || !rdr.result.length) { - alert('the selected file is not readable') - writeLabel(`Local file: unreadable!`) - } - else { + if (! rdr.result || !rdr.result.length) { + alert('the selected file is not readable'); + writeLabel(`Local file: unreadable!`); + } else { // we might have a previous graph opened - TW.resetGraph() + TW.resetGraph(); // run - mainStartGraph(theFormat, rdr.result, TW.instance) + mainStartGraph(theFormat, rdr.result, TW.instance); // NB 3rd arg null = we got no additional conf for this "unknown" file - writeLabel(`Local file: ${clientLocalGraphFile.name}`) + writeLabel(`Local file: ${clientLocalGraphFile.name}`); } - } - rdr.readAsText(clientLocalGraphFile) + }; + rdr.readAsText(clientLocalGraphFile); } - } + }; - var filechooserBox = document.createElement('div') - filechooserBox.appendChild(inputComment) - filechooserBox.appendChild(graphFileInput) + let filechooserBox = document.createElement('div'); + filechooserBox.appendChild(inputComment); + filechooserBox.appendChild(graphFileInput); - return filechooserBox + return filechooserBox; } -//============================ < NEW BUTTONS > =============================// +// ============================ < NEW BUTTONS > =============================// function changeType(optionaltypeFlag) { - // RELATION TYPES // // SOURCE NODE TARGET NODE @@ -367,123 +357,123 @@ function changeType(optionaltypeFlag) { // | // updates the sliders and selection panels - let typeFlag - let outgoing = TW.SystemState() - let oldTypeId = outgoing.activetypes.indexOf(true) - let mixedState = (outgoing.activereltypes.length > 1) + let typeFlag; + let outgoing = TW.SystemState(); + let oldTypeId = outgoing.activetypes.indexOf(true); + let mixedState = (outgoing.activereltypes.length > 1); // needed selection content diagnostic for mixed meso target choice - let selectionTypeId = false + let selectionTypeId = false; if (outgoing.selectionNids.length) { if (!mixedState) { - selectionTypeId = oldTypeId - } - else if (!outgoing.level) { - let selMajorityType = TW.categories[oldTypeId] - let counts = {} + selectionTypeId = oldTypeId; + } else if (!outgoing.level) { + let selMajorityType = TW.categories[oldTypeId]; + let counts = {}; for (var j in outgoing.selectionNids) { - let ty = TW.Nodes[outgoing.selectionNids[j]].type - if (! counts[ty]) counts[ty] = 1 - else counts[ty] += 1 + let ty = TW.Nodes[outgoing.selectionNids[j]].type; + if (! counts[ty]) counts[ty] = 1; + else counts[ty] += 1; } for (var ty in counts) { if (counts[ty] > counts[selMajorityType]) { - selMajorityType = ty + selMajorityType = ty; } } - selectionTypeId = TW.catDict[ty] + selectionTypeId = TW.catDict[ty]; } } // 1 - make the targetTypes choices if (!isUndef(optionaltypeFlag)) { - typeFlag = optionaltypeFlag - } - else { + typeFlag = optionaltypeFlag; + } else { // "comeback" case: going back from mixed view to selections majority view // (or last non-mixed view if no selection) // ---------- if (mixedState) { - typeFlag = selectionTypeId || outgoing.comingFromType || 0 + typeFlag = selectionTypeId || outgoing.comingFromType || 0; } // "jutsu" case: macrolevel opens mixed view // ------- else if (!outgoing.level) { - typeFlag = 'all' + typeFlag = 'all'; } // normal case: show the opposite type // ----------- else { - typeFlag = (oldTypeId + 1) % 2 // binary toggle next 0 => 1 + typeFlag = (oldTypeId + 1) % 2; // binary toggle next 0 => 1 // 1 => 0 } } // 2 - infer consequences of targetTypes - let newActivetypes = [] + let newActivetypes = []; if (typeFlag == 'all') { - for (var i in TW.categories) { newActivetypes.push(true) } - } - else { for (var i in TW.categories) { - if (i == typeFlag) newActivetypes.push(true) - else newActivetypes.push(false) + newActivetypes.push(true); +} + } else { + for (var i in TW.categories) { + if (i == typeFlag) newActivetypes.push(true); + else newActivetypes.push(false); } } // console.log('newActivetypes', newActivetypes) - let newReltypes = TW.instance.inferActivereltypes(newActivetypes) + let newReltypes = TW.instance.inferActivereltypes(newActivetypes); // console.log('newReltypes', newReltypes) // nodes already in target type - let alreadyOk = {} + let alreadyOk = {}; if (mixedState) { - let arr = TW.partialGraph.graph.getNodesByType(typeFlag) - for (var i in arr) {alreadyOk[arr[i]] = true} + let arr = TW.partialGraph.graph.getNodesByType(typeFlag); + for (var i in arr) { +alreadyOk[arr[i]] = true; +} } // 3 - define the projected selection (sourceNids => corresponding opposites) - let sourceNids = outgoing.selectionNids + let sourceNids = outgoing.selectionNids; // when jutsu and no selection => we pick one selection at random (in meso mode we can't go to another meso with no selection, because nothing would appear) if (typeFlag == 'all' && !sourceNids.length) { - sourceNids = [] + sourceNids = []; for (var nid in TW.Nodes) { if (! TW.Nodes[nid].hidden) { - sourceNids.push(nid) - break + sourceNids.push(nid); + break; } } } - let targetNids = {} + let targetNids = {}; if (!mixedState) { - targetNids = getNeighbors(sourceNids, 'XR') - } - else { + targetNids = getNeighbors(sourceNids, 'XR'); + } else { // in mixed local state we need to separate those already tgt state from others - let needXRTransition = [] + let needXRTransition = []; for (var i in sourceNids) { - let nid = sourceNids[i] - if (alreadyOk[nid]) targetNids[nid] = true - else needXRTransition.push(nid) + let nid = sourceNids[i]; + if (alreadyOk[nid]) targetNids[nid] = true; + else needXRTransition.push(nid); } // if none of the selection in new type => selection's projection // if some of the selection in new type => this majority subset of selection // without the projection of others if (! Object.keys(targetNids).length) { - targetNids = getNeighbors(needXRTransition, "XR") + targetNids = getNeighbors(needXRTransition, 'XR'); } // console.log("mixedState start, selections targetNids:", targetNids) } // 4 - define the nodes to be added - let newNodes = {} + let newNodes = {}; // in mode all the current selection (and only it) is preserved if (typeFlag == 'all') { for (var i in outgoing.selectionNids) { - let nid = outgoing.selectionNids[i] - newNodes[nid] = TW.Nodes[nid] + let nid = outgoing.selectionNids[i]; + newNodes[nid] = TW.Nodes[nid]; } } @@ -491,76 +481,76 @@ function changeType(optionaltypeFlag) { if (outgoing.level) { for (let typeId in newActivetypes) { if (newActivetypes[typeId]) { - newNodes = Object.assign(newNodes, getNodesOfType(typeId)) + newNodes = Object.assign(newNodes, getNodesOfType(typeId)); } } - } - else { + } else { if (Object.keys(targetNids).length) { for (var nid in targetNids) { - newNodes[nid] = TW.Nodes[nid] + newNodes[nid] = TW.Nodes[nid]; } // also more added because they are the "meso" sameside neighbors of the selection - let rel = typeFlag.toString().repeat(2) - let additionalNewTypeNids = getNeighbors(Object.keys(targetNids), rel) + let rel = typeFlag.toString().repeat(2); + let additionalNewTypeNids = getNeighbors(Object.keys(targetNids), rel); for (var nid in additionalNewTypeNids) { - newNodes[nid] = TW.Nodes[nid] + newNodes[nid] = TW.Nodes[nid]; } } // if no selection, meso shouldn't be possible, but we can still // show something: those that were already of the correct type else if (mixedState) { for (var nid in alreadyOk) { - newNodes[nid] = TW.Nodes[nid] + newNodes[nid] = TW.Nodes[nid]; } } } // console.log('newNodes', newNodes) // 5 - define the new selection - let newselsArr = [] + let newselsArr = []; if (outgoing.selectionNids.length) { if (typeFlag != 'all') { - newselsArr = Object.keys(targetNids) + newselsArr = Object.keys(targetNids); // NB: if mixedState we already filtered them at step 3 - } - else { + } else { // not extending selection to all transitive neighbors // makes the operation stable (when clicked several times, // without changing selection, we go back to original state) - newselsArr = outgoing.selectionNids + newselsArr = outgoing.selectionNids; } } // 6 - effect the changes on nodes - deselectNodes() - TW.partialGraph.graph.clear() // a new start + deselectNodes(); + TW.partialGraph.graph.clear(); // a new start for (var nid in newNodes) { try { - TW.partialGraph.graph.addNode(newNodes[nid]) - } catch(e) {continue} + TW.partialGraph.graph.addNode(newNodes[nid]); + } catch (e) { +continue; +} } // 7 - add the relations - let newEdges = {} - for (var srcnid in newNodes) { - for (var k in newReltypes) { - let relKey = newReltypes[k] + let newEdges = {}; + for (let srcnid in newNodes) { + for (let k in newReltypes) { + let relKey = newReltypes[k]; if (TW.Relations[relKey] && TW.Relations[relKey][srcnid] && TW.Relations[relKey][srcnid].length) { for (var j in TW.Relations[relKey][srcnid]) { - let tgtnid = TW.Relations[relKey][srcnid][j] + let tgtnid = TW.Relations[relKey][srcnid][j]; if (newNodes[tgtnid]) { - let eids = [`${srcnid};${tgtnid}`, `${tgtnid};${srcnid}`] - for (var l in eids) { - let eid = eids[l] - if (eid && TW.Edges[eid] && !TW.partialGraph.graph.edges(eid)){ - newEdges[eid] = TW.Edges[eid] - break + let eids = [`${srcnid};${tgtnid}`, `${tgtnid};${srcnid}`]; + for (let l in eids) { + let eid = eids[l]; + if (eid && TW.Edges[eid] && !TW.partialGraph.graph.edges(eid)) { + newEdges[eid] = TW.Edges[eid]; + break; } } } @@ -570,72 +560,73 @@ function changeType(optionaltypeFlag) { } // 8 - effect the changes on edges - for (var eid in newEdges) { + for (let eid in newEdges) { try { - TW.partialGraph.graph.addEdge(newEdges[eid]) - } catch(e) {continue} + TW.partialGraph.graph.addEdge(newEdges[eid]); + } catch (e) { +continue; +} } // 9 - refresh view and record the state - TW.partialGraph.camera.goTo({x:0, y:0, ratio:1, angle: 0}) - TW.partialGraph.refresh() + TW.partialGraph.camera.goTo({x: 0, y: 0, ratio: 1, angle: 0}); + TW.partialGraph.refresh(); - if (typeFlag != "all") { + if (typeFlag != 'all') { TW.pushGUIState({ activetypes: newActivetypes, activereltypes: newReltypes, - sels: newselsArr + sels: newselsArr, // rels: added by MS2 (highlighted opposite- and same-side neighbours) // possible: add it in an early way here and request that MS2 doesn't change state - }) - } - else { + }); + } else { TW.pushGUIState({ activetypes: newActivetypes, comingFromType: oldTypeId, activereltypes: newReltypes, - sels: newselsArr - }) + sels: newselsArr, + }); } // to recreate the new selection in the new type graph, if we had one before // NB relies on new actypes so should be after pushState if (newselsArr.length) { - TW.instance.selNgn.MultipleSelection2({nodes: newselsArr}); - if (TW.conf.debug.logSelections) - console.log("selection transitive projection from",sourceNids, "to", newselsArr) + TW.instance.selNgn.multipleSelection2({nodes: newselsArr}); + if (TW.conf.debug.logSelections) { +console.log('selection transitive projection from', sourceNids, 'to', newselsArr); +} } // update search labels - TW.labels.splice(0, TW.labels.length) + TW.labels.splice(0, TW.labels.length); for (var nid in newNodes) { - updateSearchLabels(nid,newNodes[nid].label,newNodes[nid].type); + updateSearchLabels(nid, newNodes[nid].label, newNodes[nid].type); } // update the gui (TODO handle by TW.pushGUIState) ========================= - updateDynamicFacets() + updateDynamicFacets(); // console.log("outgoing.activetypes", outgoing.activetypes) // console.log("newActivetypes", newActivetypes) - changeGraphAppearanceByFacets( getActivetypesNames() ) + changeGraphAppearanceByFacets( getActivetypesNames() ); // turn off the altcolors for outgoing types - for (var tyId in TW.categories) { - let ty = TW.categories[tyId] + for (let tyId in TW.categories) { + let ty = TW.categories[tyId]; if (outgoing.activetypes[tyId] && ! newActivetypes[tyId]) { if (TW.gui.handpickedcolors[ty].alton) { - clearColorLegend([ty]) - TW.gui.handpickedcolors[ty].alton = false + clearColorLegend([ty]); + TW.gui.handpickedcolors[ty].alton = false; } - } - else if (!outgoing.activetypes[tyId] && newActivetypes[tyId]) { + } else if (!outgoing.activetypes[tyId] && newActivetypes[tyId]) { if (TW.gui.handpickedcolors[ty].altattr) { - TW.gui.handpickedcolors[ty].alton = true + TW.gui.handpickedcolors[ty].alton = true; // this re-coloring can be avoided if "hidden" was used in changeLevel and sliders - let recolorMethod = getColorFunction(TW.gui.handpickedcolors[ty].altattr) - window[recolorMethod](TW.gui.handpickedcolors[ty].altattr, [ty]) + let recolorMethod = getColorFunction(TW.gui.handpickedcolors[ty].altattr); + window[recolorMethod](TW.gui.handpickedcolors[ty].altattr, [ty]); // without re-coloring step, we would only need to recreate legend box // updateColorsLegend(TW.gui.handpickedcolors[ty].altattr, [ty]) @@ -643,51 +634,51 @@ function changeType(optionaltypeFlag) { } } - TW.partialGraph.settings('labelThreshold', getSizeFactor()) - fillAttrsInForm('choose-attr') - fillAttrsInForm('attr-titling-metric', 'num') + TW.partialGraph.settings('labelThreshold', getSizeFactor()); + fillAttrsInForm('choose-attr'); + fillAttrsInForm('attr-titling-metric', 'num'); // recreates FA2 nodes array from new nodes reInitFa2({ useSoftMethod: false, callback: function() { - sigma_utils.smartForceAtlas() - } - }) + sigma_utils.smartForceAtlas(); + }, + }); // end update the gui ====================================================== } // the pool of available nodes of a given type -function getNodesOfType (typeid){ - let res = {} +function getNodesOfType(typeid) { + let res = {}; if (TW.ByType[typeid]) { - for (var j in TW.ByType[typeid]) { - let nid = TW.ByType[typeid][j] - let n = TW.Nodes[TW.ByType[typeid][j]] - res[nid] = n + for (let j in TW.ByType[typeid]) { + let nid = TW.ByType[typeid][j]; + let n = TW.Nodes[TW.ByType[typeid][j]]; + res[nid] = n; } } - return res + return res; } // one transitive step function getNeighbors(sourceNids, relKey) { - let targetDict = {} - for (var i in sourceNids) { - let srcnid = sourceNids[i] + let targetDict = {}; + for (let i in sourceNids) { + let srcnid = sourceNids[i]; if (TW.Relations[relKey] && !isUndef(TW.Relations[relKey][srcnid]) && TW.Relations[relKey][srcnid].length) { - for (var j in TW.Relations[relKey][srcnid]) { - let tgtnid = TW.Relations[relKey][srcnid][j] - targetDict[tgtnid] = true + for (let j in TW.Relations[relKey][srcnid]) { + let tgtnid = TW.Relations[relKey][srcnid][j]; + targetDict[tgtnid] = true; } } } - return targetDict + return targetDict; } @@ -703,28 +694,25 @@ function getNeighbors(sourceNids, relKey) { // (=> avoid redoing property computations and state push) // POSS: rewrite using .hidden instead of add/remove function changeLevel(optionalTgtState) { - // show waiting cursor TW.gui.elHtml.classList.add('waiting'); // let the waiting cursor appear setTimeout(function() { - // array of nids [144, 384, 543] - var sels + let sels; if (optionalTgtState) { - sels = optionalTgtState.selectionNids - } - else { + sels = optionalTgtState.selectionNids; + } else { var present = TW.SystemState(); // Last - sels = present.selectionNids + sels = present.selectionNids; } - deselectNodes() + deselectNodes(); - let selsChecker = {} + let selsChecker = {}; for (let i in sels) { - selsChecker[sels[i]] = true + selsChecker[sels[i]] = true; } // type "grammar" @@ -734,92 +722,91 @@ function changeLevel(optionalTgtState) { // [true, true] <=> '1|1' if (optionalTgtState) { - activetypes = optionalTgtState.activetypes - activereltypes = optionalTgtState.activereltypes - } - else { + activetypes = optionalTgtState.activetypes; + activereltypes = optionalTgtState.activereltypes; + } else { activetypes = present.activetypes; activereltypes = present.activereltypes; } - let activetypesDict = {} + let activetypesDict = {}; for (var i in activetypes) { if (activetypes[i]) { - activetypesDict[TW.categories[i]] = true + activetypesDict[TW.categories[i]] = true; } } TW.partialGraph.graph.clear(); - var voisinage = {} + let voisinage = {}; // Dictionaries of: selection+neighbors - var nodesToAdd = {} - var edgesToAdd = {} + let nodesToAdd = {}; + let edgesToAdd = {}; - for(var i in sels) { + for (var i in sels) { s = sels[i]; nodesToAdd[s]=true; for (var k in activereltypes) { - let activereltype = activereltypes[k] + let activereltype = activereltypes[k]; if (TW.Relations[activereltype]) { - neigh = TW.Relations[activereltype][s] - if(neigh) { - for(var j in neigh) { - t = neigh[j] + neigh = TW.Relations[activereltype][s]; + if (neigh) { + for (var j in neigh) { + t = neigh[j]; nodesToAdd[t]=true; - edgesToAdd[s+";"+t]=true; - edgesToAdd[t+";"+s]=true; - if( !selsChecker[t] ) - voisinage[ t ] = true; + edgesToAdd[s+';'+t]=true; + edgesToAdd[t+';'+s]=true; + if ( !selsChecker[t] ) { +voisinage[t] = true; +} } } - } - else { + } else { // case where no edges at all (ex: scholars have no common keywords) - console.log("no edges between these nodes") + console.log('no edges between these nodes'); } } } - var futurelevel = optionalTgtState ? optionalTgtState.level : !present.level - - if(!futurelevel) { // [Change to Local] when level=Global(1) + let futurelevel = optionalTgtState ? optionalTgtState.level : !present.level; - TW.gui.elContainer.style.backgroundColor = TW.conf.mesoBackground + if (!futurelevel) { // [Change to Local] when level=Global(1) + TW.gui.elContainer.style.backgroundColor = TW.conf.mesoBackground; - for(var nid in nodesToAdd) - add1Elem(nid) - for(var eid in edgesToAdd) { - add1Elem(eid) + for (var nid in nodesToAdd) { +add1Elem(nid); +} + for (var eid in edgesToAdd) { + add1Elem(eid); } // Adding intra-neighbors edges O(voisinage²) - voisinage = Object.keys(voisinage) - for(var i=0;i