var DBG = DBG || false; var DBG1 = true; var DBG_INIT_SELECTED = [ "BI_audit_ENERGA_RUM_KONTRAHENCI.18661", "BI_audit_KRS.389967" ]; if ('undefined' === typeof HTML_ID_REF_GRAPH) throw "Missing HTML_ID_REF_GRAPH"; if (!RAPORT_ID) throw "Missing RAPORT_ID"; if (!API_URL) throw "Missing API_URL"; if (!global.p5VendorJs) throw "Missing p5VendorJs"; if (!global.vis) throw "Missing vis"; if (!global.p5VendorJs.Typeahead) throw "Missing Typeahead"; var createReactClass = global.p5VendorJs.createReactClass; var h = global.p5VendorJs.React.createElement; var React = global.p5VendorJs.React; var ReactDOM = global.p5VendorJs.ReactDOM; var Redux = global.p5VendorJs.Redux; var ReduxThunk = global.p5VendorJs.ReduxThunk; var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js var Typeahead = global.p5VendorJs.Typeahead; var mapStatsNodeToGraphNode = function (node) { return { id: node['@object'] + '.' + node['@primaryKey'], label: node['@label'], value: 1, level: 0, } }; var mapStatsEdgeToGraphEdge = function (edge) { return { id: Utils.makeUniqueEdgeId(edge['source'], edge['target']), from: edge['source'], to: edge['target'], pathId: edge['pathId'], listPathId: [ edge['pathId'] ], } }; //// DBG: Utils: // var from, to; // from = 'aa.11'; to = 'bb.22'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') }) // from = 'bb.22'; to = 'aa.11'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') }) // from = 'cc.22'; to = 'cc.11'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') }) var Utils = (function __makeUtils() { var Utils = {}; var _cacheListFeatureNames = []; Utils.fidSplit = function (fid) { var dotPos = fid.indexOf('.'); return [ fid.substr(0, dotPos), fid.substr(dotPos + 1) ]; } Utils.makeUniqueEdgeId = function (from, to) { var listFidArr = [ from, to ].sort().map(Utils.fidSplit); var listIdx = listFidArr.map(function (fidArr) { var cacheIdx = _cacheListFeatureNames.indexOf(fidArr[0]); if (-1 === cacheIdx) { cacheIdx = _cacheListFeatureNames.length; _cacheListFeatureNames.push(fidArr[0]); } return cacheIdx; }) return listIdx.map(function (cacheIdx, fidIdx) { return cacheIdx + '.' + listFidArr[fidIdx][1]; }).join('-') } Utils.getCacheAllEdgeId = function () { return [].concat(_cacheListFeatureNames); } return Utils; })(); { // TODO: RaportOutputPanel - mv to another file var RaportOutputPanel = {}; RaportOutputPanel.initialState = function () { var nodes = (STATS && STATS.nodes) ? STATS.nodes : []; var edges = (STATS && STATS.edges) ? STATS.edges : []; var paths = (STATS && STATS.path_list) ? STATS.path_list : []; DBG1 && console.log('DBG: STATS', STATS); return { nodes: nodes.map(mapStatsNodeToGraphNode), edges: edges.map(mapStatsEdgeToGraphEdge), paths: paths, isLoading: false, sentRequestId: 0, receivedRequestId: 0, } }; RaportOutputPanel.store = function (state, action) { var prevState = state || RaportOutputPanel.initialState(); DBG1 && console.log('DBG: store', { prevState, action, actionType: action.type }); switch (action.type) { case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, { sentRequestId: action.sentRequestId, isLoading: true }); case 'SET_RESPONSE': return Object.assign(prevState, { receivedRequestId: action.requestId, nodes: action.body.nodes.map(mapStatsNodeToGraphNode), edges: action.body.edges, isLoading: (prevState.sentRequestId > action.requestId) ? true : false, }); default: return prevState; } }; RaportOutputPanel.createActions = function () { var setSentRequestId = function (sentRequestId) { return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId }; } var setResponse = function (response) { return Object.assign(response, { type: 'SET_RESPONSE' }); } var fetchData = function (raportId, params) { return function (dispatch, getState) { var state = getState(); var this__requestId = state.sentRequestId + 1; dispatch(setSentRequestId(this__requestId)); var reqPromise = window.fetch(API_URL, { method: 'GET', credentials: 'same-origin', }).then(function (response) { return response.json(); }); return reqPromise.then(function (response) { var items = response; var state = getState(); if (state.receivedRequestId > this__requestId) { DBG1 && console.log('DBG: skipped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId }); return; } dispatch( setResponse({ requestId: this__requestId, msg: "Pobrano dane", body: response.body }) ); }).catch(function (e) { p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e}); // dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button }); } } return { fetchData: fetchData, } }; } { // TODO: NetworkGraph - mv to another file var NetworkGraph = {}; NetworkGraph.initialState = function () { var nodes = (STATS && STATS.nodes) ? STATS.nodes : []; var edges = (STATS && STATS.edges) ? STATS.edges : []; var paths = (STATS && STATS.path_list) ? STATS.path_list : []; DBG1 && console.log('DBG: STATS', STATS); return { nodes: nodes.map(mapStatsNodeToGraphNode), edges: edges.map(mapStatsEdgeToGraphEdge), paths: paths, isLoading: false, sentRequestId: 0, receivedRequestId: 0, } }; NetworkGraph.store = function (state, action) { var prevState = state || NetworkGraph.initialState(); DBG1 && console.log('DBG: store', { prevState, action, actionType: action.type }); switch (action.type) { case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, { sentRequestId: action.sentRequestId, isLoading: true }); case 'SET_RESPONSE': return Object.assign(prevState, { receivedRequestId: action.requestId, nodes: action.body.nodes.map(mapStatsNodeToGraphNode), edges: action.body.edges, isLoading: (prevState.sentRequestId > action.requestId) ? true : false, }); default: return prevState; } }; NetworkGraph.createActions = function () { var setSentRequestId = function (sentRequestId) { return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId }; } var setResponse = function (response) { return Object.assign(response, { type: 'SET_RESPONSE' }); } var fetchData = function (raportId, params) { return function (dispatch, getState) { var state = getState(); var this__requestId = state.sentRequestId + 1; dispatch(setSentRequestId(this__requestId)); var reqPromise = window.fetch(API_URL, { method: 'GET', credentials: 'same-origin', }).then(function (response) { return response.json(); }); return reqPromise.then(function (response) { var items = response; var state = getState(); if (state.receivedRequestId > this__requestId) { DBG1 && console.log('DBG: skipped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId }); return; } dispatch( setResponse({ requestId: this__requestId, msg: "Pobrano dane", body: response.body }) ); }).catch(function (e) { p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e}); // dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button }); } } return { fetchData: fetchData, } }; } var defaultNetworkGraphOptions = { nodes: { shape: 'dot', scaling: { min: 8, max: 30, label: { min: 8, max: 16, drawThreshold: 6, maxVisible: 20 } }, font: { color: "#666", size: 10, face: 'Helvetica Neue, Helvetica, Arial' } // font: "8px Helvetica Neue, Helvetica, Arial" }, interaction: { hover: true, hoverConnectedEdges: false, selectConnectedEdges: true, }, physics: { barnesHut: { avoidOverlap: 0.2 } }, }; var TMP_COUNTER = 1; var p5UI__NetworkGraph = createReactClass({ _network: null, _visOutputRef: React.createRef(), _allNodes: null, // new vis.DataSet(), _allEdges: null, // new vis.DataSet(), _nodes: new vis.DataSet(), _edges: new vis.DataSet(), getStateFromStore: function () { var state = this.props.store.getState(); return { isLoading: state.isLoading, receivedRequestId: state.receivedRequestId, // to force render after udpate nodes, edges } }, getInitialState: function () { if (this.props.selected) this._updateSelected(this.props.selected); return { initialized: false, isLoading: false, receivedRequestId: null, }; }, setOutputRef: function (elem) { this._visOutputRef = elem; }, componentDidMount: function () { var data = { nodes: this._nodes, edges: this._edges }; this._network = new vis.Network(this._visOutputRef, data, defaultNetworkGraphOptions); if (this.props.onZoom) this._network.on('zoom', this.props.onZoom) this.setState({ initialized: true }); this._unsubscribe = this.props.store.subscribe(this._storeUpdated) }, _storeUpdated: function () { this.setState( this.getStateFromStore() ) }, fitToContainer: function () { if (this._network) { this._network.fit() } }, incScale: function () { if (this._network) { var scale = this._network.getScale(); this._network.moveTo({ scale: scale + 0.2 }) } }, decScale: function () { if (this._network) { var scale = this._network.getScale(); this._network.moveTo({ scale: (scale > 0.3) ? scale - 0.2 : scale }) } }, reloadGraph: function () { if (this._network) { // this._network.setData({ nodes: [], edges: [] }); // var edges = this._edges.getDataSet(); var networkFit = (function (network) { var _network = network; return function () { DBG1 && console.log('DBG:networkFit'); _network.fit(); } })(this._network); // { // var edges = this._edges.map(function(e) { return e; }); // DataSet to Array // this._edges.clear(); // var animAddEdges = (function (this__edges, edges, wait) { // return function (callback) { // DBG1 && console.log('DBG:animAddEdges', { edges }); // if (edges.length > 0) { // var edge = edges.pop(); // this__edges.add(edge); // setTimeout(function () { // animAddEdges(callback) // }, wait); // } // } // })(this._edges, edges, 10); // animAddEdges(animAddEdges); // } { var nodes = this._nodes.map(function(e) { return e; }); // DataSet to Array this._nodes.clear(); var animAddNodes = (function (this__nodes, nodes, wait, onFinishCallback) { return function (callback) { DBG1 && console.log('DBG:animAddNodes', { nodes }); if (nodes.length > 0) { var node = nodes.pop(); this__nodes.add(node); setTimeout(function () { animAddNodes(callback) }, wait); } else if (onFinishCallback) { setTimeout(function () { onFinishCallback() }, 500); } } })(this._nodes, nodes, 200, networkFit); animAddNodes(animAddNodes); } // DBG1 && console.log('DBG: reloadGraph', { 'this._nodes': this._nodes, 'this._edges': this._edges, edges: edges }); // this._network.setData({ nodes: this._nodes, edges: this._edges }); // this._network.fit() // zoom out to fit container size } }, testOnClick1: function () { TMP_COUNTER++; this._nodes.add({ id: TMP_COUNTER, label: "Node " + TMP_COUNTER, value: TMP_COUNTER, level: 0 }); }, testOnClick2: function () { TMP_COUNTER++; var ids = this._nodes.getIds(); var totalNodes = ids.length if (totalNodes < 3) return; var fromIdx = Math.floor(Math.random() * totalNodes); var toIdx = Math.floor(Math.random() * totalNodes); this._edges.add({ from: ids[fromIdx], to: ids[toIdx] }); }, testOnClick3: function () { this.props.store.dispatch( this.props.storeActions.fetchData( this.props.raportId ) ); }, shouldComponentUpdate: function (nextProps, nextState) { // if (this.state.receivedRequestId !== nextState.receivedRequestId) { // add missing nodes // var state = this.props.store.getState(); // DBG1 && console.warn("TODO: update nodes, edges", { state }); // // this._edges.add( edge_or_edges_array ) // if (state.nodes && state.nodes.length) { // var __nodes = this._nodes; // state.nodes.forEach(function (node) { // __nodes.add(node); // }) // } // // edge: { from: page, to: subpageID, color:getEdgeColor(level), level: level, selectionWidth:2, hoverWidth:0 } // if (state.edges && state.edges.length) { // var __edges = this._edges; // // state.edges.forEach(function (edge) { // state.edges.slice(0, 30).forEach(function (edge) { // __edges.add({ // from: edge.source, // to: edge.target, // }); // }) // } // } if (this.props.selected.length != nextProps.selected.length) { this._updateSelected(nextProps.selected) } return false; }, _updateSelected: function (selected) { // TODO: move to RaportOutputPanel Store var state = this.props.store.getState(); var idsSelected = (selected || []).map( function (node) { return node.id } ) var nodes = []; // var edges = state.edges.filter(function (edge) { // use idsSelected - TODO: RMME // return ( -1 !== idsSelected.indexOf(edge.source) || -1 !== idsSelected.indexOf(edge.target) ); // }) { // TODO: find all paths with selected fids var foundPaths = state.paths.filter(function (path) { for (var i = 0, totalFids = path.fids.length, fid = null; i < totalFids; i++) { fid = path.fids[i]; DBG && console.log('DBG:foundPaths ', {fid: ''+fid, path}); if ( -1 !== idsSelected.indexOf(fid) ) return true; } return false; }) // TODO: add missing nodes (not selected but on path) var addFidsToGraph = Array.from(new Set(foundPaths.reduce(function (ret, path) { return ret.concat(path.fids.filter(function (fid) { return ( -1 === idsSelected.indexOf(fid) ); })) }, []))); // TODO: view only combined path or all not selected nodes? - trying: not selected nodes // TODO: convert found paths to graph edges } DBG1 && console.warn('DBG: TODO set edges by selectd nodes...', { state_nodes: state.nodes, state_edges: state.edges, idsSelected, foundPaths, addFidsToGraph }); this._nodes = new vis.DataSet(selected); { // TODO: convert fid to node on found list { // if (!this._allNodes) { this._allNodes = new vis.DataSet(); this._allNodes.add(state.nodes); } var this__allNodes = this._allNodes; var __nodes = this._nodes; var addNodesToGraph = addFidsToGraph.map(function (fid) { var node = this__allNodes.get(fid) try { DBG1 && console.log('DBG: TODO add node (fid:'+fid+') ', { node, this__allNodes, fid }); __nodes.add(node); __nodes.update(Object.assign(node, { color: '#ddd', size: 100 })); DBG1 && console.log('DBG: added red node (fid:'+fid+') ', { node }); } catch (e) { DBG1 && console.log(e); DBG1 && console.log('DBG: skip node (fid:'+fid+') already added'); } }) } DBG1 && console.log('DBG: check edges: ', { 'this._edges.length': this._edges.length, 'state.edges.length': state.edges.length }); if (this._edges.length != state.edges.length) { DBG1 && console.log('DBG: TODO: update edges', { 'state.edges': state.edges }); var this__edges = this._edges; state.edges.forEach(function (edge) { var foundEdge = this__edges.get(edge.id); if (!foundEdge) { this__edges.add(edge); } else { var listPathId = (-1 !== foundEdge.listPathId.indexOf(edge.pathId)) ? foundEdge.listPathId : foundEdge.listPathId.concat(edge.pathId); var mergedEdge = Object.assign(foundEdge, { listPathId: listPathId, width: 1 + (listPathId.length - 1) / 10, }); this__edges.update(mergedEdge); } }) } if (this._network) { this._network.setData({ nodes: this._nodes, edges: this._edges }); this._network.fit() // zoom out to fit container size } }, render: function () { DBG1 && console.log('DBG:render'); return h('div', { style: { 'position': "relative" } }, [ h('div', { ref: this.setOutputRef, style: { 'min-height': 600, 'height': 600, 'border-radius': "6px", 'border': "1px solid #ddd", }, }), h('div', { className: "", style: { position: "absolute", right: 10, top: 10 } }, [ h('div', { className: "btn-group-vertical", title: "Powiększenie" }, [ h('button', { onClick: this.incScale, className: "btn btn-xs btn-default" }, [ h('i', { className: "glyphicon glyphicon-plus" }) ]), h('button', { onClick: this.fitToContainer, className: "btn btn-xs btn-default" }, [ "100%" ]), h('button', { onClick: this.decScale, className: "btn btn-xs btn-default" }, [ h('i', { className: "glyphicon glyphicon-minus" }) ]), ]), h('div', { style: { clear: "both", margin: 4 } }), h('div', { className: "btn-group-vertical", style: { width: "100%" } }, [ h('button', { onClick: this.reloadGraph, className: "btn btn-xs btn-default", title: "Odśwież" }, [ h('i', { className: "glyphicon glyphicon-random" }) ]), ]), ]), h('div', {}, [ h('button', { onClick: this.testOnClick1 }, [ "TEST 1 ", h('small', [], "(+ node)") ]), h('button', { onClick: this.testOnClick2 }, [ "TEST 2 ", h('small', [], "(+ edge)") ]), h('button', { onClick: this.testOnClick3 }, [ "TEST 3 ", h('small', [], "(+ fetch)") ]), ]), ]); } }); /** * props.store: networkGraphStore * - used: state.nodes */ var p5UI__RaportOutputPanel = createReactClass({ getInitialState: function () { var initSelected = []; if (DBG_INIT_SELECTED) { var state = this.props.store.getState() initSelected = state.nodes.filter(function (node) { return ( -1 !== DBG_INIT_SELECTED.indexOf(node.id)); }) } return { selected: initSelected, } }, handleSelectFeatures: function (selected) { DBG1 && console.log('DBG:typeahead selected:', { selected }) this.setState({ selected: selected }); }, _onZoom: function (event) { // { // direction: '+'/'-', // scale: Number, // pointer: {x:pointer_x, y:pointer_y} // } DBG1 && console.log('DBG:onZoom', { scale: event.scale, event }) var this__handleZoomUpdate = this.handleZoomUpdate; }, handleZoomUpdate: function () { DBG1 && console.warn('DBG:handleZoomUpdate', { event }) }, render: function () { var state = this.props.store.getState() DBG1 && console.log('DBG: state', state); var nodes = state.nodes || []; return h(React.Fragment, {}, [ h(Typeahead, { labelKey: "label", multiple: true, options: nodes, placeholder: "Wybierz", bsSize: 'large', selected: this.state.selected, onChange: this.handleSelectFeatures, }), h(p5UI__NetworkGraph, { raportId: this.props.raportId, store: this.props.store, storeActions: this.props.storeActions, selected: this.state.selected, onZoom: window._.throttle(this._onZoom, 500), }) ]) } }) ReactDOM.render( h(p5UI__RaportOutputPanel, { raportId: RAPORT_ID, store: createStoreWithThunkMiddleware(NetworkGraph.store), storeActions: NetworkGraph.createActions(), }), document.getElementById(HTML_ID_REF_GRAPH) );