diff --git a/modules/demo/ssr/graph/index.js b/modules/demo/ssr/graph/index.js index c038048d1..75d6c6e93 100755 --- a/modules/demo/ssr/graph/index.js +++ b/modules/demo/ssr/graph/index.js @@ -15,10 +15,22 @@ // limitations under the License. const fastify = require('fastify')(); +const fs = require('fs'); +const path = require('path'); + +// create `/data` directory if it does not exist +const basePath = path.join(__dirname, 'data/'); +fs.access(basePath, fs.constants.F_OK, (err, _) => () => { + if (!err) { fs.mkdir(basePath); } +}); fastify // .register(require('./plugins/webrtc'), require('./plugins/graph')(fastify)) .register(require('fastify-static'), {root: require('path').join(__dirname, 'public')}) - .get('/', (req, reply) => reply.sendFile('video.html')); + .register(require('fastify-multipart')) + .register(require('fastify-cors'), {}) + .register((require('fastify-arrow'))) + .register(require('./plugins/api')) + .get('/', (req, reply) => reply.sendFile('video.html')) fastify.listen(8080).then(() => console.log('server ready')); diff --git a/modules/demo/ssr/graph/package.json b/modules/demo/ssr/graph/package.json index c9cfb891d..8963a5690 100644 --- a/modules/demo/ssr/graph/package.json +++ b/modules/demo/ssr/graph/package.json @@ -17,18 +17,24 @@ "@rapidsai/cuda": "0.0.1", "@rapidsai/deck.gl": "0.0.1", "@rapidsai/jsdom": "0.0.1", + "@rapidsai/ssr-render-cluster": "0.0.1", "fastify-plugin": "3.0.0", "fastify-socket.io": "2.0.0", "fastify-static": "4.4.1", "fastify": "3.20.2", - "nanoid": "3.1.31", + "fastify-multipart": "5.0.2", + "fastify-cors": "6.0.2", + "fastify-arrow": "0.1.0", + "apache-arrow": "^4.0.0", + "nanoid": "3.1.25", "rxjs": "6.6.7", "shm-typed-array": "0.0.13", "simple-peer": "9.11.0", - "socket.io": "4.1.3" + "socket.io": "4.1.3", + "glob": "7.2.0", + "sharp": "0.29.2" }, "files": [ - "render", "public", "plugins", "index.js", diff --git a/modules/demo/ssr/graph/plugins/api/index.js b/modules/demo/ssr/graph/plugins/api/index.js new file mode 100644 index 000000000..5938b7322 --- /dev/null +++ b/modules/demo/ssr/graph/plugins/api/index.js @@ -0,0 +1,165 @@ +// Copyright (c) 2021, NVIDIA CORPORATION. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const {graphs, clients} = require('../graph'); +const fs = require('fs') +const util = require('util') +const {pipeline} = require('stream') +const pump = util.promisify(pipeline) +const glob = require('glob'); +const {Graph} = require('@rapidsai/cugraph'); +const {loadEdges, loadNodes} = require('../graph/loader'); +const {RecordBatchStreamWriter} = require('apache-arrow'); +const path = require('path'); +const {readDataFrame, getNodesForGraph, getEdgesForGraph, getPaginatedRows} = require('./utils'); + +module.exports = function(fastify, opts, done) { + fastify.addHook('preValidation', (request, reply, done) => { + // handle upload validation after reading request.file() in the route function itself + if (request.url == '/datasets/upload') { + done(); + } else { + request.query.id = + (request.method == 'POST') ? `${request.body.id}:video` : `${request.query.id}:video`; + if (request.query.id in fastify[clients]) { + done(); + } else { + reply.code(500).send('client handshake not established'); + } + } + }); + + async function renderGraph(id, data) { + const asDeviceMemory = (buf) => new (buf[Symbol.species])(buf); + const src = data.edges.dataframe.get(data.edges.src); + const dst = data.edges.dataframe.get(data.edges.dst); + const graph = Graph.fromEdgeList(src, dst, {directedEdges: true}); + fastify[graphs][id] = { + refCount: 0, + nodes: await getNodesForGraph(asDeviceMemory, data.nodes, graph.numNodes), + edges: await getEdgesForGraph(asDeviceMemory, data.edges), + graph: graph, + }; + + ++fastify[graphs][id].refCount; + + return { + gravity: 0.0, + linLogMode: false, + scalingRatio: 5.0, + barnesHutTheta: 0.0, + jitterTolerance: 0.05, + strongGravityMode: false, + outboundAttraction: false, + graph: fastify[graphs][id].graph, + nodes: { + ...fastify[graphs][id].nodes, + length: fastify[graphs][id].graph.numNodes, + }, + edges: { + ...fastify[graphs][id].edges, + length: fastify[graphs][id].graph.numEdges, + }, + }; + } + + fastify.post('/datasets/upload', async function(req, reply) { + const data = await req.file(); + const id = `${data.fields.id.value}:video`; + if (id in fastify[clients]) { + const basePath = `${__dirname}/../../data/`; + const filepath = path.join(basePath, data.filename); + const target = fs.createWriteStream(filepath); + try { + await pump(data.file, target); + } catch (err) { console.log(err); } + reply.send(); + } else { + reply.code(500).send('client handshake not established'); + } + }); + + fastify.get('/datasets', async (request, reply) => { + glob(`*.{csv,parquet}`, + {cwd: `${__dirname}/../../data/`}, + (er, files) => { reply.send(JSON.stringify(files.concat(['defaultExample']))); }); + }); + + fastify.post('/dataframe/load', async (request, reply) => { + const filePath = `${__dirname}/../../data/` + if (fs.existsSync(`${filePath}${request.body.nodes}`) && + fs.existsSync(`${filePath}${request.body.edges}`)) { + fastify[clients][request.query.id].data.nodes.dataframe = + await readDataFrame(`${filePath}${request.body.nodes}`); + + fastify[clients][request.query.id].data.edges.dataframe = + await readDataFrame(`${filePath}${request.body.edges}`); + } + else { + fastify[clients][request.query.id].data.nodes.dataframe = await loadNodes(); + fastify[clients][request.query.id].data.edges.dataframe = await loadEdges(); + } + if (fastify[clients][request.query.id].data.nodes.dataframe.numRows == 0) { + reply.code(500).send('no dataframe loaded'); + } + reply.send(JSON.stringify({ + 'nodes': fastify[clients][request.query.id].data.nodes.dataframe.numRows, + 'edges': fastify[clients][request.query.id].data.edges.dataframe.numRows + })); + }) + + fastify.get('/dataframe/columnNames/read', async (request, reply) => { + reply.send(JSON.stringify({ + nodesParams: fastify[clients][request.query.id].data.nodes.dataframe.names.concat([null]), + edgesParams: fastify[clients][request.query.id].data.edges.dataframe.names.concat([null]) + })); + }); + + fastify.post('/dataframe/columnNames/update', async (request, reply) => { + try { + Object.assign(fastify[clients][request.query.id].data.nodes, request.body.nodes); + Object.assign(fastify[clients][request.query.id].data.edges, request.body.edges); + reply.code(200).send('successfully updated columnNames'); + } catch (err) { reply.code(500).send(err); } + }); + + fastify.post('/graph/render', async (request, reply) => { + try { + fastify[clients][request.query.id].graph = + await renderGraph('default', fastify[clients][request.query.id].data); + reply.code(200).send('successfully rendered graph'); + } catch (err) { reply.code(500).send(err); } + }) + + fastify.get('/dataframe/read', async (request, reply) => { + try { + const pageIndex = parseInt(request.query.pageIndex); + const pageSize = parseInt(request.query.pageSize); + const dataframe = request.query.dataframe; //{'nodes', 'edges'} + const [arrowTable, numRows] = + await getPaginatedRows(fastify[clients][request.query.id].data[dataframe].dataframe, + pageIndex, + pageSize, + fastify[clients][request.query.id].state.selectedInfo[dataframe]); + + arrowTable.schema.metadata.set('numRows', numRows); + RecordBatchStreamWriter.writeAll(arrowTable).pipe(reply.stream()); + } catch (err) { + request.log.error({err}, '/run_query error'); + reply.code(500).send(err); + } + }); + + done(); +} diff --git a/modules/demo/ssr/graph/plugins/api/utils.js b/modules/demo/ssr/graph/plugins/api/utils.js new file mode 100644 index 000000000..1d27c73c0 --- /dev/null +++ b/modules/demo/ssr/graph/plugins/api/utils.js @@ -0,0 +1,95 @@ +const {DataFrame, Series, Int32, Uint8, Uint32, Uint64} = require('@rapidsai/cudf'); +const {Float32Buffer} = require('@rapidsai/cuda'); + +function readDataFrame(path) { + if (path.indexOf('.csv', path.length - 4) !== -1) { + // csv file + return DataFrame.readCSV({sources: [path], header: 0, sourceType: 'files'}); + } else if (path.indexOf('.parquet', path.length - 8) !== -1) { + // csv file + return DataFrame.readParquet({sources: [path]}); + } + // if (df.names.includes('Unnamed: 0')) { df = df.cast({'Unnamed: 0': new Uint32}); } + return new DataFrame({}); +} + +async function getNodesForGraph(asDeviceMemory, nodes, numNodes) { + let nodesRes = {}; + const pos = new Float32Buffer(Array.from( + {length: numNodes * 2}, + () => Math.random() * 1000 * (Math.random() < 0.5 ? -1 : 1), + )); + + if (nodes.x in nodes.dataframe.names) { + nodesRes.nodeXPositions = asDeviceMemory(nodes.dataframe.get(node.x).data); + } else { + nodesRes.nodeXPositions = pos.subarray(0, pos.length / 2); + } + if (nodes.y in nodes.dataframe.names) { + nodesRes.nodeYPositions = asDeviceMemory(nodes.dataframe.get(node.y).data); + } else { + nodesRes.nodeYPositions = pos.subarray(pos.length / 2); + } + if (nodes.dataframe.names.includes(nodes.size)) { + nodesRes.nodeRadius = asDeviceMemory(nodes.dataframe.get(nodes.size).cast(new Uint8).data); + } + if (nodes.dataframe.names.includes(nodes.color)) { + nodesRes.nodeFillColors = + asDeviceMemory(nodes.dataframe.get(nodes.color).cast(new Uint32).data); + } + if (nodes.dataframe.names.includes(nodes.id)) { + nodesRes.nodeElementIndices = + asDeviceMemory(nodes.dataframe.get(nodes.id).cast(new Uint32).data); + } + return nodesRes; +} + +async function getEdgesForGraph(asDeviceMemory, edges) { + let edgesRes = {}; + + if (edges.dataframe.names.includes(edges.color)) { + edgesRes.edgeColors = asDeviceMemory(edges.dataframe.get(edges.color).data); + } else { + edgesRes.edgeColors = asDeviceMemory( + Series + .sequence( + {type: new Uint64, size: edges.dataframe.numRows, init: 18443486512814075489n, step: 0}) + .data); + } + if (edges.dataframe.names.includes(edges.id)) { + edgesRes.edgeList = asDeviceMemory(edges.dataframe.get(edges.id).cast(new Uint64).data); + } + if (edges.dataframe.names.includes(edges.bundle)) { + edgesRes.edgeBundles = asDeviceMemory(edges.dataframe.get(edges.bundle).data); + } + return edgesRes; +} + +async function getPaginatedRows(df, pageIndex = 0, pageSize = 400, selected = []) { + if (selected.length != 0) { + const selectedSeries = Series.new({type: new Int32, data: selected}).unique(true); + const updatedDF = df.gather(selectedSeries); + const idxs = Series.sequence({ + type: new Int32, + init: (pageIndex - 1) * pageSize, + size: Math.min(pageSize, updatedDF.numRows), + step: 1 + }); + return [updatedDF.gather(idxs).toArrow(), updatedDF.numRows]; + } else { + const idxs = Series.sequence({ + type: new Int32, + init: (pageIndex - 1) * pageSize, + size: Math.min(pageSize, df.numRows), + step: 1 + }); + return [df.gather(idxs).toArrow(), df.numRows]; + } +} + +module.exports = { + readDataFrame, + getNodesForGraph, + getEdgesForGraph, + getPaginatedRows +} diff --git a/modules/demo/ssr/graph/render/render.js b/modules/demo/ssr/graph/plugins/graph/deck.js similarity index 64% rename from modules/demo/ssr/graph/render/render.js rename to modules/demo/ssr/graph/plugins/graph/deck.js index 5035460e1..b2bf12477 100644 --- a/modules/demo/ssr/graph/render/render.js +++ b/modules/demo/ssr/graph/plugins/graph/deck.js @@ -11,97 +11,17 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - const {IpcMemory, Uint8Buffer} = require('@rapidsai/cuda'); -const {RapidsJSDOM} = require('@rapidsai/jsdom'); -const copyFramebuffer = require('./copy')(); - -class Renderer { - constructor() { - const onAnimationFrameRequested = immediateAnimationFrame(this); - const jsdom = new RapidsJSDOM({module, onAnimationFrameRequested}); - - const {deck, render} = jsdom.window.evalFn(makeDeck); - - this.deck = deck; - this.jsdom = jsdom; - this._render = render; - } - async render(props = {}, graph = {}, state = {}, events = [], frame = 0) { - const window = this.jsdom.window; - - graph = openGraphIpcHandles(graph); - props && this.deck.setProps(props); - - state?.deck && this.deck.restore(state.deck); - state?.graph && Object.assign(graph, state.graph); - state?.window && Object.assign(window, state.window); - - state?.selectedInfo && Object.assign(this.deck.selectedInfo, state.selectedInfo); - state?.boxSelectCoordinates && - Object.assign(this.deck.boxSelectCoordinates, state.boxSelectCoordinates); - - (events || []).forEach((event) => window.dispatchEvent(event)); - - await this._render(graph, - this.deck.boxSelectCoordinates.rectdata, - state.pickingMode === 'boxSelect' ? {controller: {dragPan: false}} - : {controller: {dragPan: true}}); - - closeIpcHandles(graph.data.nodes); - closeIpcHandles(graph.data.edges); - return { - frame: copyFramebuffer(this.deck.animationLoop, frame), - state: { - deck: this.deck.serialize(), - graph: this.deck.layerManager.getLayers() - ?.find((layer) => layer.id === 'GraphLayer') - .serialize(), - window: { - x: window.x, - y: window.y, - title: window.title, - width: window.width, - height: window.height, - cursor: window.cursor, - mouseX: window.mouseX, - mouseY: window.mouseY, - buttons: window.buttons, - scrollX: window.scrollX, - scrollY: window.scrollY, - modifiers: window.modifiers, - mouseInWindow: window.mouseInWindow, - }, - boxSelectCoordinates: this.deck.boxSelectCoordinates, - selectedInfo: this.deck.selectedInfo - } - }; - } -} - -module.exports.Renderer = Renderer; - -function immediateAnimationFrame(renderer) { - let request = null; - let flushing = false; - const flush = () => { - flushing = true; - while (request && request.active) { - const f = request.flush; - request = null; - f(); - } - flushing = false; - }; - return (r) => { - if (flushing) { return request = r; } - if (renderer?.deck?.animationLoop?._initialized) { // - return flush(request = r); - } - if (!request && (request = r)) { setImmediate(flush); } - }; -} +/** + * makeDeck() returns a Deck and a render callable object to be consumed by the multi-worker + * Renderer class' JSDOM object + * + * @returns { + * DeckSSR, + * render(layers = {}, boxSelectRectData = [], props = {}) + * } + */ function makeDeck() { const {log: deckLog} = require('@deck.gl/core'); deckLog.level = 0; @@ -144,7 +64,7 @@ function makeDeck() { ]; }; - getPolygonLayer = (rectdata) => { + const getPolygonLayer = (rectdata) => { return new PolygonLayer({ filled: true, stroked: true, @@ -189,17 +109,13 @@ function makeDeck() { layerIds: ['GraphLayer'] }; - deck.selectedInfo.selectedNodes = deck.pickObjects(deck.selectedInfo.selectedCoordinates) - .filter(selected => selected.hasOwnProperty('nodeId')) - .map(n => n.nodeId); + deck.selectedInfo.nodes = deck.pickObjects(deck.selectedInfo.selectedCoordinates) + .filter(selected => selected.hasOwnProperty('nodeId')) + .map(n => n.nodeId); - deck.selectedInfo.selectedEdges = deck.pickObjects(deck.selectedInfo.selectedCoordinates) - .filter(selected => selected.hasOwnProperty('edgeId')) - .map(n => n.edgeId); - console.log('selected Nodes', - deck.selectedInfo.selectedNodes, - '\nselected Edges', - deck.selectedInfo.selectedEdges); + deck.selectedInfo.edges = deck.pickObjects(deck.selectedInfo.selectedCoordinates) + .filter(selected => selected.hasOwnProperty('edgeId')) + .map(n => n.edgeId); } const onDrag = (info, event) => { @@ -219,12 +135,11 @@ function makeDeck() { y: info.y, radius: 1, }; - deck.selectedInfo.selectedNodes = - [deck.pickObject(deck.selectedInfo.selectedCoordinates)] - .filter(selected => selected && selected.hasOwnProperty('nodeId')) - .map(n => n.nodeId); + deck.selectedInfo.nodes = [deck.pickObject(deck.selectedInfo.selectedCoordinates)] + .filter(selected => selected && selected.hasOwnProperty('nodeId')) + .map(n => n.nodeId); - console.log(deck.selectedInfo.selectedNodes, deck.selectedInfo.selectedCoordinates); + console.log(deck.selectedInfo.nodes, deck.selectedInfo.selectedCoordinates); }; const deck = new DeckSSR({ @@ -259,11 +174,13 @@ function makeDeck() { return { deck, - render(graph, rectdata, cb_props = {}) { + render(layers = {}) { const done = deck.animationLoop.waitForRender(); deck.setProps({ - layers: makeLayers(deck, graph).concat(rectdata[0].show ? getPolygonLayer(rectdata) : []), - ...cb_props + layers: makeLayers(deck, layers) + .concat(deck.boxSelectCoordinates.rectdata[0].show + ? getPolygonLayer(deck.boxSelectCoordinates.rectdata) + : []), }); deck.animationLoop.start(); return done; @@ -293,6 +210,11 @@ function openGraphIpcHandles({nodes, edges, ...graphLayerProps} = {}) { }; } +function closeGraphIpcHandles(graph) { + closeIpcHandles(graph.data.nodes); + closeIpcHandles(graph.data.edges); +} + function openNodeIpcHandles(attrs = {}) { const attributes = { nodeRadius: openIpcHandle(attrs.nodeRadius), @@ -335,3 +257,14 @@ function closeIpcHandles(obj) { } } } + +function serializeCustomLayer(layers = []) { + return layers?.find((layer) => layer.id === 'GraphLayer').serialize(); +} + +module.exports = { + makeDeck: makeDeck, + openLayerIpcHandles: openGraphIpcHandles, + closeLayerIpcHandles: closeGraphIpcHandles, + serializeCustomLayer: serializeCustomLayer +}; diff --git a/modules/demo/ssr/graph/plugins/graph/index.js b/modules/demo/ssr/graph/plugins/graph/index.js index 7853448ac..becdacf96 100644 --- a/modules/demo/ssr/graph/plugins/graph/index.js +++ b/modules/demo/ssr/graph/plugins/graph/index.js @@ -12,17 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -const wrtc = require('wrtc'); -const {DeviceBuffer} = require('@rapidsai/rmm'); -const {MemoryView} = require('@rapidsai/cuda'); -const {Float32Buffer} = require('@rapidsai/cuda'); -const {Graph} = require('@rapidsai/cugraph'); -const {Series, Int32} = require('@rapidsai/cudf'); - -const {loadNodes, loadEdges} = require('./loader'); -const {RenderCluster} = require('../../render/cluster'); +const wrtc = require('wrtc'); +const {MemoryView, DeviceMemory, Float32Buffer} = require('@rapidsai/cuda'); +const {DataFrame, Series, Float32} = require('@rapidsai/cudf'); +const {RenderCluster} = require('@rapidsai/ssr-render-cluster'); const {create: shmCreate, detach: shmDetach} = require('shm-typed-array'); +const asDeviceMemory = (buf) => new (buf[Symbol.species])(buf); module.exports = graphSSRClients; module.exports.graphs = Symbol('graphs'); @@ -47,7 +43,7 @@ function graphSSRClients(fastify) { const { width = 800, height = 600, - layout = true, + layout = false, g: graphId = 'default', } = sock?.handshake?.query || {}; @@ -64,24 +60,14 @@ function graphSSRClients(fastify) { }, event: {}, props: {width, height, layout}, - graph: await loadGraph(graphId), + graph: {}, + data: { + nodes: {dataframe: new DataFrame({}), color: '', size: '', id: '', x: 'x', y: 'y'}, + edges: {dataframe: new DataFrame({}), color: '', id: '', bundle: '', src: 'src', dst: 'dst'} + }, frame: shmCreate(width * height * 3 / 2), peer: peer, }; - if (clients[stream.id].graph.dataframes[0]) { - const res = getPaginatedRows(clients[stream.id].graph.dataframes[0]); - peer.send(JSON.stringify({ - type: 'data', - data: {nodes: {data: res, length: clients[stream.id].graph.dataframes[0].numRows}} - })); - } - if (clients[stream.id].graph.dataframes[1]) { - const res = getPaginatedRows(clients[stream.id].graph.dataframes[1]); - peer.send(JSON.stringify({ - type: 'data', - data: {edges: {data: res, length: clients[stream.id].graph.dataframes[1].numRows}} - })); - } stream.addTrack(source.createTrack()); peer.streams.push(stream); @@ -127,82 +113,23 @@ function graphSSRClients(fastify) { } } } - - async function loadGraph(id) { - let dataframes = []; - - if (!(id in graphs)) { - const asDeviceMemory = (buf) => new (buf[Symbol.species])(buf); - dataframes = await Promise.all([loadNodes(id), loadEdges(id)]); - const src = dataframes[1].get('src'); - const dst = dataframes[1].get('dst'); - graphs[id] = { - refCount: 0, - nodes: { - nodeRadius: asDeviceMemory(dataframes[0].get('size').data), - nodeFillColors: asDeviceMemory(dataframes[0].get('color').data), - nodeElementIndices: asDeviceMemory(dataframes[0].get('id').data), - }, - edges: { - edgeList: asDeviceMemory(dataframes[1].get('edge').data), - edgeColors: asDeviceMemory(dataframes[1].get('color').data), - edgeBundles: asDeviceMemory(dataframes[1].get('bundle').data), - }, - graph: Graph.fromEdgeList(src, dst), - }; - } - - ++graphs[id].refCount; - - const pos = new Float32Buffer(Array.from( - {length: graphs[id].graph.numNodes * 2}, - () => Math.random() * 1000 * (Math.random() < 0.5 ? -1 : 1), - )); - - return { - gravity: 0.0, - linLogMode: false, - scalingRatio: 5.0, - barnesHutTheta: 0.0, - jitterTolerance: 0.05, - strongGravityMode: false, - outboundAttraction: false, - graph: graphs[id].graph, - nodes: { - ...graphs[id].nodes, - length: graphs[id].graph.numNodes, - nodeXPositions: pos.subarray(0, pos.length / 2), - nodeYPositions: pos.subarray(pos.length / 2), - }, - edges: { - ...graphs[id].edges, - length: graphs[id].graph.numEdges, - }, - dataframes: dataframes - }; - } } function layoutAndRenderGraphs(clients) { - const renderer = new RenderCluster({numWorkers: 1 && 4}); + const renderer = new RenderCluster( + {numWorkers: 1 && 4, deckLayersPath: require('path').join(__dirname, 'deck')}); return () => { for (const id in clients) { const client = clients[id]; - const sendToClient = - ([nodes, edges]) => { - client.peer.send(JSON.stringify( - {type: 'data', data: {nodes: {data: getPaginatedRows(nodes), length: nodes.numRows}}})); - client.peer.send(JSON.stringify( - {type: 'data', data: {edges: {data: getPaginatedRows(edges), length: edges.numRows}}})); - } - if (client.isRendering) { - continue; - } + if (client.isRendering) { continue; } const state = {...client.state}; - const props = {...client.props}; + const props = { + ...client.props, + controller: (state.pickingMode === 'boxSelect' ? {dragPan: false} : {dragPan: true}) + }; const event = [ 'focus', @@ -247,7 +174,7 @@ function layoutAndRenderGraphs(clients) { props, event, frame: client.frame.key, - graph: { + layers: { ...client.graph, graph: undefined, edges: getIpcHandles(client.graph.edges), @@ -263,26 +190,16 @@ function layoutAndRenderGraphs(clients) { result.state.clearSelections = false; // reset selected state - result.state.selectedInfo.selectedNodes = []; - result.state.selectedInfo.selectedEdges = []; + result.state.selectedInfo.nodes = []; + result.state.selectedInfo.edges = []; result.state.selectedInfo.selectedCoordinates = {}; result.state.boxSelectCoordinates.rectdata = [{polygon: [[]], show: false}]; // send to client - if (client.graph.dataframes) { sendToClient(client.graph.dataframes); } + client.peer.send(JSON.stringify({type: 'data', data: 'newQuery'})); } else if (JSON.stringify(client.state.selectedInfo.selectedCoordinates) !== JSON.stringify(result.state.selectedInfo.selectedCoordinates)) { - // selections updated - const nodes = - Series.new({type: new Int32, data: result.state.selectedInfo.selectedNodes}); - const edges = - Series.new({type: new Int32, data: result.state.selectedInfo.selectedEdges}); - if (client.graph.dataframes) { - sendToClient([ - client.graph.dataframes[0].gather(nodes), - client.graph.dataframes[1].gather(edges) - ]); - } + client.peer.send(JSON.stringify({type: 'data', data: 'newQuery'})); } // copy result state to client's current state result?.state && Object.assign(client.state, result.state); @@ -294,16 +211,23 @@ function layoutAndRenderGraphs(clients) { } } -function getPaginatedRows(df, page = 1, rowsPerPage = 400) { - if (!df) { return {}; } - return df.head(page * rowsPerPage).tail(rowsPerPage).toArrow().toArray(); -} - function forceAtlas2({graph, nodes, edges, ...params}) { - graph.forceAtlas2({...params, positions: nodes.nodeXPositions.buffer}); + if (graph == undefined) { return {}; } + const asDeviceMemory = (buf) => new (buf[Symbol.species])(buf); + + const positions = graph.forceAtlas2({...params, positions: nodes.nodeXPositions.data}); + + nodes.nodeXPositions = asDeviceMemory( + Series + .new( + {type: new Float32, length: graph.numNodes, offset: graph.numNodes, data: positions.buffer}) + .data); + nodes.nodeYPositions = asDeviceMemory( + Series.new({type: new Float32, length: graph.numNodes, offset: 0, data: positions.buffer}) + .data); return { - graph, + graph: graph, ...params, nodes: {...nodes, length: graph.numNodes}, edges: {...edges, length: graph.numEdges}, diff --git a/modules/demo/ssr/graph/plugins/graph/loader.js b/modules/demo/ssr/graph/plugins/graph/loader.js index 023e0209d..29d0e07a3 100644 --- a/modules/demo/ssr/graph/plugins/graph/loader.js +++ b/modules/demo/ssr/graph/plugins/graph/loader.js @@ -1,5 +1,5 @@ -const {Utf8Vector} = require('apache-arrow'); -const {DataFrame, Series, Uint32, Uint64, Uint8} = require('@rapidsai/cudf'); +const {Utf8Vector} = require('apache-arrow'); +const {DataFrame, Series, Int32, Uint32, Uint64, Uint8} = require('@rapidsai/cudf'); function loadNodes(graphId) { if (graphId === 'default') { return getDefaultNodes(); } @@ -84,7 +84,7 @@ function getDefaultEdges() { return new DataFrame({ name: Series.new(Utf8Vector.from(Array.from({length: 312}, (_, i) => `${i}`))), src: Series.new({ - type: new Uint32, + type: new Int32, data: [ 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, @@ -103,7 +103,7 @@ function getDefaultEdges() { ] }), dst: Series.new({ - type: new Uint32, + type: new Int32, data: [ 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 8, 9, 10, 11, 12, 14, 15, 16, 13, 17, 18, 17, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 35, 35, 35, 36, 37, diff --git a/modules/demo/ssr/graph/public/video.html b/modules/demo/ssr/graph/public/video.html index c2c25edbb..0662ec30a 100644 --- a/modules/demo/ssr/graph/public/video.html +++ b/modules/demo/ssr/graph/public/video.html @@ -1,3 +1,14 @@ + + + + + + + + + + + @@ -18,11 +29,13 @@
+ +
-