console.log('jxr extras to gradually add, by default could refer to branches instead then add as components proper') console.log('arguably utils and additional interactions beyond pinche could be extras rather than core') console.log('extra could also be empty...') // from https://glitch.com/edit/#!/zen-pim?path=graph-cyto-headless.html // see also https://observablehq.com/@utopiah/d3-pim-graph // motivated by https://these.arthurperret.fr/chapitre-4.html#cosmographe-et-cosmoscope const cytoJson = "https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph_cyto.json" var cy var jsonLoaded fetch(cytoJson) .then(function (response) { return response.json(); }) .then(function (json) { // take nodes from wiki.Nodes then construct an array in elements for cytoscape format // or directly from JSON http://js.cytoscape.org/#notation/elements-json jsonLoaded = json startCytoWithData(json) }) .catch(function (err) { console.log(err); }); // should instead directly use https://vatelier.net/MyDemo/newtooling/wiki_graph_cyto.json // generated from https://vatelier.net/MyDemo/newtooling/build_graph_cytograph.js // cf instead http://js.cytoscape.org/demos/tokyo-railways/tokyo-railways.json more detail // with details on how to load the JSON function runCytoAnalysis(){ console.log('----------------- network analysis started --------------') if (!cy.nodes('[id = "Analysis.Analysis"]').length) { console.warn('Wrong dataset, cancelling') return } // example of queries var aStar = cy.elements().aStar({ root: '[id = "Analysis.Analysis"]', goal: '[id = "Analysis.CostsAndBenefitsOfSocietalMembership"]' }) if ( aStar.path){ console.log("Path from Analysis.Analysis to Analysis.CostsAndBenefitsOfSocietalMembership", aStar.path.edges() , aStar.path.nodes() ) aStar.path.select() } console.log( cy.nodes('[id = "Analysis.Analysis"]').neighborhood() ) before = Date.now() var bc = cy.elements().bc() after = Date.now() console.log("took ", after-before, "ms to run.") console.log( 'bc of j: ' + bc.betweenness('[id = "Analysis.Analysis"]') ) console.log( cy.nodes('[id = "Analysis.CostsAndBenefitsOfSocietalMembership"]').neighborhood() ) //no need to run cy.elements().bc() again. It's done once for the whole graph. console.log( 'bc of j: ' + bc.betweenness('[id = "Analysis.CostsAndBenefitsOfSocietalMembership"]') ) console.log('----------------- network analysis done -----------------') } function startCytoWithData(json){ cy = cytoscape({ headless:true, elements: json.elements }) //runCytoAnalysis() //quite demanding, skipped for now. let defaults = { name: 'euler', springLength: edge => 80, springCoeff: edge => 0.0008, mass: node => 4, gravity: -1.2, pull: 0.001, theta: 0.666, dragCoeff: 0.02, movementThreshold: 1, timeStep: 20, refresh: 10, animate: true, animationDuration: undefined, animationEasing: undefined, maxIterations: 1000, maxSimulationTime: 4000, ungrabifyWhileSimulating: false, fit: true, padding: 30, // Constrain layout bounds with one of // - { x1, y1, x2, y2 } // - { x1, y1, w, h } // - undefined / null : Unconstrained boundingBox: undefined, // Layout event callbacks; equivalent to `layout.one('layoutready', callback)` for example ready: function(){ console.log("graph ready", cy.json()) }, // on layoutready stop: stableGraph(), // on layoutstop randomize: false }; // disabled for tests //cy.layout( defaults ).run(); // too demanding for the entire graph, should limit to a subset } function stableGraph(){ var exportableJSON = cy.json() console.log( 'exportable JSON', exportableJSON ); // not actually stable! // still could be used as a form of caching BUT... would take into account new nodes added since var node = "ReadingNotes.ApocalypticAI" if (!cy.nodes('[id = "'+node+'"]').length) { console.warn('Wrong dataset, cancelling') return } console.log('should add/update AFrame nodes e.g', cy.elements('[id = "'+node+'"]').position()) console.log('could add all the nodes then their links with proper attributes in order to do select() after') // using e.g. cy.nodes().forEach( n => console.log(n.data(), n.position() ) ) // cy.edges().forEach( e => console.log(e.data(), e.sourceEndpoint(), e.targetEndpoint() )) // warning, very costly! // run on entire wiki though whereas previous D3 instance limited to 10 pages and their targets } let edges_to_display = [] function displayLeafs(graph, graphEl, rootId, rootEl, depth=3){ console.log( graph[rootId].Id ) if (depth<1) return graph[rootId].Targets.map( l => { console.log( "-", graph[l].Id ) let x = addNodeFromGraph(graph[l].Id, "" + (Math.random()*2) + " " + (Math.random()*2) + " -" + (Math.random()*2) ) // layout could be done with Cytoscape but somehow seems I can't use it properly, in headless mode or not. x.setAttribute('update-links-on-pinchended', true) x.setAttribute('toggle-links-on-left-pinchended', true) console.log("linking:", rootEl, x) edges_to_display.push({graphel:graphEl, source:rootEl, target:x}) displayLeafs( graph, graphEl, graph[l].Id, x, --depth) }) } AFRAME.registerComponent('toggle-links-on-left-pinchended', { init: function(){ let graphEl = document.querySelector("#graphroot") let el = this.el this.el.addEventListener('lreleased', function (event) { // if it has children (...how knowing that we are not using the hierarchy?) // delete them, including links (should be recursive too) // if not, displayLeafs( mynodes, graphEl, root.Id, rootEl, 1) // assumes not for now displayLeafs( wikiStructure, graphEl, el.getAttribute("value"), el, 1) setTimeout( _ => { edges_to_display.map( i => addEdgeBetweenNodesFromGraph( i.graphel, i.source, i.target ))}, 2000 ) }) } }) AFRAME.registerComponent('update-links-on-pinchended', { init: function(){ let rootEl = document.querySelector("#graphroot") let el = this.el this.el.addEventListener('released', function (event) { Object.keys( rootEl.components ) // get all links .filter( i => i.indexOf(el.id) > -1 ) // keeps links related to the moved node .map( i => { // for each link let newpos = AFRAME.utils.coordinates.stringify( el.getAttribute("position") ) let [src,tgt] = i.replace("line__","").split("__to__"); srcpos = AFRAME.utils.coordinates.stringify( rootEl.getAttribute(i).start ) tgtpos = AFRAME.utils.coordinates.stringify( rootEl.getAttribute(i).end ) if (src==el.id){ rootEl.setAttribute(i, { start: newpos, end: tgtpos }) } else { rootEl.setAttribute(i, { start: srcpos, end: newpos }) } }) }) } }) let nodes = [] let edges = [] let wikiStructure = null fetch('https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json').then( r => r.json() ).then( r => displayGraph(r.Nodes) ); // alternatively there is the issue component that could also display linked issues function displayGraph(mynodes){ wikiStructure = mynodes let startingNode = "Wiki.VirtualRealityInterface" let root = mynodes[startingNode] let graphEl = document.createElement("a-entity") graphEl.id = "graphroot" AFRAME.scenes[0].appendChild( graphEl ) let node_names = Object.keys( mynodes ) let rootEl = addNodeFromGraph(root.Id, "" + (Math.random()*2-1) + " " + (Math.random()+1) + " -" + (Math.random()*2-0.5) ) rootEl.setAttribute('update-links-on-pinchended', true) displayLeafs( mynodes, graphEl, root.Id, rootEl, 2) setTimeout( _ => { edges_to_display.map( i => addEdgeBetweenNodesFromGraph( i.graphel, i.source, i.target ))}, 2000 ) // quite unreliable // should listen to an event instead to insure that nodes are all created before } function addNodeFromGraph(name, position="0 0 0"){ // add sphere, with its name, make it a target // define what "it" is knowing we can't move a children with an offset // consequently parenting should be done by the text let el = addNewNote(name, position, ".1 .1 .1", "node_" +crypto.randomUUID(), "node_from_graph") let sphereEl = document.createElement("a-sphere") sphereEl.setAttribute("radius", .1) sphereEl.setAttribute("segments-height", 4) sphereEl.setAttribute("segments-width", 4) sphereEl.setAttribute("wireframe", true) sphereEl.setAttribute("position", "0 -.1 0") el.appendChild(sphereEl) // position shouldn't have to be offset return el } function addEdgeBetweenNodesFromGraph(graphEl, a, b){ // a.setAttribute( "line-link-entities", {source: a.id, target: b.id} ) doesn't seem work, back to basics for now graphEl.setAttribute("line__"+a.id+"__to__"+b.id, { start: AFRAME.utils.coordinates.stringify( a.getAttribute("position") ) , end: AFRAME.utils.coordinates.stringify( b.getAttribute("position") ) }) // not that this doesn't take into account the parent node moving }