BiAuditGraph.php.network-graph.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. var DBG = DBG || false;
  2. var DBG1 = true;
  3. var DBG_INIT_SELECTED = [ "BI_audit_ENERGA_RUM_KONTRAHENCI.18661", "BI_audit_KRS.389967" ];
  4. if ('undefined' === typeof HTML_ID_REF_GRAPH) throw "Missing HTML_ID_REF_GRAPH";
  5. if (!RAPORT_ID) throw "Missing RAPORT_ID";
  6. if (!API_URL) throw "Missing API_URL";
  7. if (!global.p5VendorJs) throw "Missing p5VendorJs";
  8. if (!global.vis) throw "Missing vis";
  9. if (!global.p5VendorJs.Typeahead) throw "Missing Typeahead";
  10. var createReactClass = global.p5VendorJs.createReactClass;
  11. var h = global.p5VendorJs.React.createElement;
  12. var React = global.p5VendorJs.React;
  13. var ReactDOM = global.p5VendorJs.ReactDOM;
  14. var Redux = global.p5VendorJs.Redux;
  15. var ReduxThunk = global.p5VendorJs.ReduxThunk;
  16. var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
  17. var Typeahead = global.p5VendorJs.Typeahead;
  18. var mapStatsNodeToGraphNode = function (node) {
  19. return {
  20. id: node['@object'] + '.' + node['@primaryKey'],
  21. label: node['@label'],
  22. value: 1,
  23. level: 0,
  24. }
  25. };
  26. var mapStatsEdgeToGraphEdge = function (edge) {
  27. return {
  28. id: Utils.makeUniqueEdgeId(edge['source'], edge['target']),
  29. from: edge['source'],
  30. to: edge['target'],
  31. pathId: edge['pathId'],
  32. listPathId: [ edge['pathId'] ],
  33. }
  34. };
  35. //// DBG: Utils:
  36. // var from, to;
  37. // from = 'aa.11'; to = 'bb.22'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') })
  38. // from = 'bb.22'; to = 'aa.11'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') })
  39. // from = 'cc.22'; to = 'cc.11'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') })
  40. var Utils = (function __makeUtils() {
  41. var Utils = {};
  42. var _cacheListFeatureNames = [];
  43. Utils.fidSplit = function (fid) {
  44. var dotPos = fid.indexOf('.');
  45. return [ fid.substr(0, dotPos), fid.substr(dotPos + 1) ];
  46. }
  47. Utils.makeUniqueEdgeId = function (from, to) {
  48. var listFidArr = [ from, to ].sort().map(Utils.fidSplit);
  49. var listIdx = listFidArr.map(function (fidArr) {
  50. var cacheIdx = _cacheListFeatureNames.indexOf(fidArr[0]);
  51. if (-1 === cacheIdx) {
  52. cacheIdx = _cacheListFeatureNames.length;
  53. _cacheListFeatureNames.push(fidArr[0]);
  54. }
  55. return cacheIdx;
  56. })
  57. return listIdx.map(function (cacheIdx, fidIdx) {
  58. return cacheIdx + '.' + listFidArr[fidIdx][1];
  59. }).join('-')
  60. }
  61. Utils.getCacheAllEdgeId = function () {
  62. return [].concat(_cacheListFeatureNames);
  63. }
  64. return Utils;
  65. })();
  66. { // TODO: RaportOutputPanel - mv to another file
  67. var RaportOutputPanel = {};
  68. RaportOutputPanel.initialState = function () {
  69. var nodes = (STATS && STATS.nodes) ? STATS.nodes : [];
  70. var edges = (STATS && STATS.edges) ? STATS.edges : [];
  71. var paths = (STATS && STATS.path_list) ? STATS.path_list : [];
  72. DBG1 && console.log('DBG: STATS', STATS);
  73. return {
  74. nodes: nodes.map(mapStatsNodeToGraphNode),
  75. edges: edges.map(mapStatsEdgeToGraphEdge),
  76. paths: paths,
  77. isLoading: false,
  78. sentRequestId: 0,
  79. receivedRequestId: 0,
  80. }
  81. };
  82. RaportOutputPanel.store = function (state, action) {
  83. var prevState = state || RaportOutputPanel.initialState();
  84. DBG1 && console.log('DBG: store', { prevState, action, actionType: action.type });
  85. switch (action.type) {
  86. case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, {
  87. sentRequestId: action.sentRequestId,
  88. isLoading: true
  89. });
  90. case 'SET_RESPONSE': return Object.assign(prevState, {
  91. receivedRequestId: action.requestId,
  92. nodes: action.body.nodes.map(mapStatsNodeToGraphNode),
  93. edges: action.body.edges,
  94. isLoading: (prevState.sentRequestId > action.requestId) ? true : false,
  95. });
  96. default: return prevState;
  97. }
  98. };
  99. RaportOutputPanel.createActions = function () {
  100. var setSentRequestId = function (sentRequestId) {
  101. return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId };
  102. }
  103. var setResponse = function (response) {
  104. return Object.assign(response, { type: 'SET_RESPONSE' });
  105. }
  106. var fetchData = function (raportId, params) {
  107. return function (dispatch, getState) {
  108. var state = getState();
  109. var this__requestId = state.sentRequestId + 1;
  110. dispatch(setSentRequestId(this__requestId));
  111. var reqPromise = window.fetch(API_URL, {
  112. method: 'GET',
  113. credentials: 'same-origin',
  114. }).then(function (response) {
  115. return response.json();
  116. });
  117. return reqPromise.then(function (response) {
  118. var items = response;
  119. var state = getState();
  120. if (state.receivedRequestId > this__requestId) {
  121. DBG1 && console.log('DBG: skipped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId });
  122. return;
  123. }
  124. dispatch(
  125. setResponse({
  126. requestId: this__requestId,
  127. msg: "Pobrano dane",
  128. body: response.body
  129. })
  130. );
  131. }).catch(function (e) {
  132. p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e});
  133. // dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button
  134. });
  135. }
  136. }
  137. return {
  138. fetchData: fetchData,
  139. }
  140. };
  141. }
  142. { // TODO: NetworkGraph - mv to another file
  143. var NetworkGraph = {};
  144. NetworkGraph.initialState = function () {
  145. var nodes = (STATS && STATS.nodes) ? STATS.nodes : [];
  146. var edges = (STATS && STATS.edges) ? STATS.edges : [];
  147. var paths = (STATS && STATS.path_list) ? STATS.path_list : [];
  148. DBG1 && console.log('DBG: STATS', STATS);
  149. return {
  150. nodes: nodes.map(mapStatsNodeToGraphNode),
  151. edges: edges.map(mapStatsEdgeToGraphEdge),
  152. paths: paths,
  153. isLoading: false,
  154. sentRequestId: 0,
  155. receivedRequestId: 0,
  156. }
  157. };
  158. NetworkGraph.store = function (state, action) {
  159. var prevState = state || NetworkGraph.initialState();
  160. DBG1 && console.log('DBG: store', { prevState, action, actionType: action.type });
  161. switch (action.type) {
  162. case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, {
  163. sentRequestId: action.sentRequestId,
  164. isLoading: true
  165. });
  166. case 'SET_RESPONSE': return Object.assign(prevState, {
  167. receivedRequestId: action.requestId,
  168. nodes: action.body.nodes.map(mapStatsNodeToGraphNode),
  169. edges: action.body.edges,
  170. isLoading: (prevState.sentRequestId > action.requestId) ? true : false,
  171. });
  172. default: return prevState;
  173. }
  174. };
  175. NetworkGraph.createActions = function () {
  176. var setSentRequestId = function (sentRequestId) {
  177. return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId };
  178. }
  179. var setResponse = function (response) {
  180. return Object.assign(response, { type: 'SET_RESPONSE' });
  181. }
  182. var fetchData = function (raportId, params) {
  183. return function (dispatch, getState) {
  184. var state = getState();
  185. var this__requestId = state.sentRequestId + 1;
  186. dispatch(setSentRequestId(this__requestId));
  187. var reqPromise = window.fetch(API_URL, {
  188. method: 'GET',
  189. credentials: 'same-origin',
  190. }).then(function (response) {
  191. return response.json();
  192. });
  193. return reqPromise.then(function (response) {
  194. var items = response;
  195. var state = getState();
  196. if (state.receivedRequestId > this__requestId) {
  197. DBG1 && console.log('DBG: skipped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId });
  198. return;
  199. }
  200. dispatch(
  201. setResponse({
  202. requestId: this__requestId,
  203. msg: "Pobrano dane",
  204. body: response.body
  205. })
  206. );
  207. }).catch(function (e) {
  208. p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e});
  209. // dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button
  210. });
  211. }
  212. }
  213. return {
  214. fetchData: fetchData,
  215. }
  216. };
  217. }
  218. var defaultNetworkGraphOptions = {
  219. nodes: {
  220. shape: 'dot',
  221. scaling: {
  222. min: 8, max: 30,
  223. label: {
  224. min: 8, max: 16, drawThreshold: 6, maxVisible: 20
  225. }
  226. },
  227. font: { color: "#666", size: 10, face: 'Helvetica Neue, Helvetica, Arial' }
  228. // font: "8px Helvetica Neue, Helvetica, Arial"
  229. },
  230. interaction: {
  231. hover: true,
  232. hoverConnectedEdges: false,
  233. selectConnectedEdges: true,
  234. },
  235. physics: {
  236. barnesHut: {
  237. avoidOverlap: 0.2
  238. }
  239. },
  240. };
  241. var TMP_COUNTER = 1;
  242. var p5UI__NetworkGraph = createReactClass({
  243. _network: null,
  244. _visOutputRef: React.createRef(),
  245. _allNodes: null, // new vis.DataSet(),
  246. _allEdges: null, // new vis.DataSet(),
  247. _nodes: new vis.DataSet(),
  248. _edges: new vis.DataSet(),
  249. getStateFromStore: function () {
  250. var state = this.props.store.getState();
  251. return {
  252. isLoading: state.isLoading,
  253. receivedRequestId: state.receivedRequestId, // to force render after udpate nodes, edges
  254. }
  255. },
  256. getInitialState: function () {
  257. if (this.props.selected) this._updateSelected(this.props.selected);
  258. return {
  259. initialized: false,
  260. isLoading: false,
  261. receivedRequestId: null,
  262. };
  263. },
  264. setOutputRef: function (elem) {
  265. this._visOutputRef = elem;
  266. },
  267. componentDidMount: function () {
  268. var data = { nodes: this._nodes, edges: this._edges };
  269. this._network = new vis.Network(this._visOutputRef, data, defaultNetworkGraphOptions);
  270. if (this.props.onZoom) this._network.on('zoom', this.props.onZoom)
  271. this.setState({ initialized: true });
  272. this._unsubscribe = this.props.store.subscribe(this._storeUpdated)
  273. },
  274. _storeUpdated: function () {
  275. this.setState( this.getStateFromStore() )
  276. },
  277. fitToContainer: function () {
  278. if (this._network) {
  279. this._network.fit()
  280. }
  281. },
  282. incScale: function () {
  283. if (this._network) {
  284. var scale = this._network.getScale();
  285. this._network.moveTo({
  286. scale: scale + 0.2
  287. })
  288. }
  289. },
  290. decScale: function () {
  291. if (this._network) {
  292. var scale = this._network.getScale();
  293. this._network.moveTo({
  294. scale: (scale > 0.3) ? scale - 0.2 : scale
  295. })
  296. }
  297. },
  298. testOnClick1: function () {
  299. TMP_COUNTER++;
  300. this._nodes.add({ id: TMP_COUNTER, label: "Node " + TMP_COUNTER, value: TMP_COUNTER, level: 0 });
  301. },
  302. testOnClick2: function () {
  303. TMP_COUNTER++;
  304. var ids = this._nodes.getIds();
  305. var totalNodes = ids.length
  306. if (totalNodes < 3) return;
  307. var fromIdx = Math.floor(Math.random() * totalNodes);
  308. var toIdx = Math.floor(Math.random() * totalNodes);
  309. this._edges.add({ from: ids[fromIdx], to: ids[toIdx] });
  310. },
  311. testOnClick3: function () {
  312. this.props.store.dispatch( this.props.storeActions.fetchData( this.props.raportId ) );
  313. },
  314. shouldComponentUpdate: function (nextProps, nextState) {
  315. // if (this.state.receivedRequestId !== nextState.receivedRequestId) { // add missing nodes
  316. // var state = this.props.store.getState();
  317. // DBG1 && console.warn("TODO: update nodes, edges", { state });
  318. // // this._edges.add( edge_or_edges_array )
  319. // if (state.nodes && state.nodes.length) {
  320. // var __nodes = this._nodes;
  321. // state.nodes.forEach(function (node) {
  322. // __nodes.add(node);
  323. // })
  324. // }
  325. // // edge: { from: page, to: subpageID, color:getEdgeColor(level), level: level, selectionWidth:2, hoverWidth:0 }
  326. // if (state.edges && state.edges.length) {
  327. // var __edges = this._edges;
  328. // // state.edges.forEach(function (edge) {
  329. // state.edges.slice(0, 30).forEach(function (edge) {
  330. // __edges.add({
  331. // from: edge.source,
  332. // to: edge.target,
  333. // });
  334. // })
  335. // }
  336. // }
  337. if (this.props.selected.length != nextProps.selected.length) {
  338. this._updateSelected(nextProps.selected)
  339. }
  340. return false;
  341. },
  342. _updateSelected: function (selected) { // TODO: move to RaportOutputPanel Store
  343. var state = this.props.store.getState();
  344. var idsSelected = (selected || []).map( function (node) { return node.id } )
  345. var nodes = [];
  346. // var edges = state.edges.filter(function (edge) { // use idsSelected - TODO: RMME
  347. // return ( -1 !== idsSelected.indexOf(edge.source) || -1 !== idsSelected.indexOf(edge.target) );
  348. // })
  349. {
  350. // TODO: find all paths with selected fids
  351. var foundPaths = state.paths.filter(function (path) {
  352. for (var i = 0, totalFids = path.fids.length, fid = null; i < totalFids; i++) {
  353. fid = path.fids[i];
  354. DBG && console.log('DBG:foundPaths ', {fid: ''+fid, path});
  355. if ( -1 !== idsSelected.indexOf(fid) ) return true;
  356. }
  357. return false;
  358. })
  359. // TODO: add missing nodes (not selected but on path)
  360. var addFidsToGraph = Array.from(new Set(foundPaths.reduce(function (ret, path) {
  361. return ret.concat(path.fids.filter(function (fid) {
  362. return ( -1 === idsSelected.indexOf(fid) );
  363. }))
  364. }, [])));
  365. // TODO: view only combined path or all not selected nodes? - trying: not selected nodes
  366. // TODO: convert found paths to graph edges
  367. }
  368. DBG1 && console.warn('DBG: TODO set edges by selectd nodes...', { state_nodes: state.nodes, state_edges: state.edges, idsSelected, foundPaths, addFidsToGraph });
  369. this._nodes = new vis.DataSet(selected);
  370. { // TODO: convert fid to node on found list
  371. { // if (!this._allNodes) {
  372. this._allNodes = new vis.DataSet();
  373. this._allNodes.add(state.nodes);
  374. }
  375. var this__allNodes = this._allNodes;
  376. var __nodes = this._nodes;
  377. var addNodesToGraph = addFidsToGraph.map(function (fid) {
  378. var node = this__allNodes.get(fid)
  379. try {
  380. DBG1 && console.log('DBG: TODO add node (fid:'+fid+') ', { node, this__allNodes, fid });
  381. __nodes.add(node);
  382. __nodes.update(Object.assign(node, { color: '#ddd', size: 100 }));
  383. DBG1 && console.log('DBG: added red node (fid:'+fid+') ', { node });
  384. } catch (e) {
  385. DBG1 && console.log(e);
  386. DBG1 && console.log('DBG: skip node (fid:'+fid+') already added');
  387. }
  388. })
  389. }
  390. DBG1 && console.log('DBG: check edges: ', { 'this._edges.length': this._edges.length, 'state.edges.length': state.edges.length });
  391. if (this._edges.length != state.edges.length) {
  392. DBG1 && console.log('DBG: TODO: update edges', { 'state.edges': state.edges });
  393. var this__edges = this._edges;
  394. state.edges.forEach(function (edge) {
  395. var foundEdge = this__edges.get(edge.id);
  396. if (!foundEdge) {
  397. this__edges.add(edge);
  398. } else {
  399. var listPathId = (-1 !== foundEdge.listPathId.indexOf(edge.pathId)) ? foundEdge.listPathId : foundEdge.listPathId.concat(edge.pathId);
  400. var mergedEdge = Object.assign(foundEdge, {
  401. listPathId: listPathId,
  402. width: 1 + (listPathId.length - 1) / 10,
  403. });
  404. this__edges.update(mergedEdge);
  405. }
  406. })
  407. }
  408. if (this._network) {
  409. this._network.setData({ nodes: this._nodes, edges: this._edges });
  410. this._network.fit() // zoom out to fit container size
  411. }
  412. },
  413. render: function () {
  414. DBG1 && console.log('DBG:render');
  415. return h('div', { style: { 'position': "relative" } }, [
  416. h('div', {
  417. ref: this.setOutputRef,
  418. style: {
  419. 'min-height': 600,
  420. 'height': 600,
  421. 'border-radius': "6px",
  422. 'border': "1px solid #ddd",
  423. },
  424. }),
  425. h('div', { className: "btn-group-vertical", style: { position: "absolute", right: 10, top: 10 } }, [
  426. h('button', { onClick: this.incScale, className: "btn btn-xs btn-default" }, [ h('i', { className: "glyphicon glyphicon-plus" }) ]),
  427. h('button', { onClick: this.fitToContainer, className: "btn btn-xs btn-default" }, [ "100%" ]),
  428. h('button', { onClick: this.decScale, className: "btn btn-xs btn-default" }, [ h('i', { className: "glyphicon glyphicon-minus" }) ]),
  429. ]),
  430. h('div', {}, [
  431. h('button', { onClick: this.testOnClick1 }, [ "TEST 1 ", h('small', [], "(+ node)") ]),
  432. h('button', { onClick: this.testOnClick2 }, [ "TEST 2 ", h('small', [], "(+ edge)") ]),
  433. h('button', { onClick: this.testOnClick3 }, [ "TEST 3 ", h('small', [], "(+ fetch)") ]),
  434. ]),
  435. ]);
  436. }
  437. });
  438. /**
  439. * props.store: networkGraphStore
  440. * - used: state.nodes
  441. */
  442. var p5UI__RaportOutputPanel = createReactClass({
  443. getInitialState: function () {
  444. var initSelected = [];
  445. if (DBG_INIT_SELECTED) {
  446. var state = this.props.store.getState()
  447. initSelected = state.nodes.filter(function (node) {
  448. return ( -1 !== DBG_INIT_SELECTED.indexOf(node.id));
  449. })
  450. }
  451. return {
  452. selected: initSelected,
  453. }
  454. },
  455. handleSelectFeatures: function (selected) {
  456. DBG1 && console.log('DBG:typeahead selected:', { selected })
  457. this.setState({ selected: selected });
  458. },
  459. _onZoom: function (event) {
  460. // {
  461. // direction: '+'/'-',
  462. // scale: Number,
  463. // pointer: {x:pointer_x, y:pointer_y}
  464. // }
  465. DBG1 && console.log('DBG:onZoom', { scale: event.scale, event })
  466. var this__handleZoomUpdate = this.handleZoomUpdate;
  467. },
  468. handleZoomUpdate: function () {
  469. DBG1 && console.warn('DBG:handleZoomUpdate', { event })
  470. },
  471. render: function () {
  472. var state = this.props.store.getState()
  473. DBG1 && console.log('DBG: state', state);
  474. var nodes = state.nodes || [];
  475. return h(React.Fragment, {}, [
  476. h(Typeahead, {
  477. labelKey: "label",
  478. multiple: true,
  479. options: nodes,
  480. placeholder: "Wybierz",
  481. bsSize: 'large',
  482. selected: this.state.selected,
  483. onChange: this.handleSelectFeatures,
  484. }),
  485. h(p5UI__NetworkGraph, {
  486. raportId: this.props.raportId,
  487. store: this.props.store,
  488. storeActions: this.props.storeActions,
  489. selected: this.state.selected,
  490. onZoom: window._.throttle(this._onZoom, 500),
  491. })
  492. ])
  493. }
  494. })
  495. ReactDOM.render(
  496. h(p5UI__RaportOutputPanel, {
  497. raportId: RAPORT_ID,
  498. store: createStoreWithThunkMiddleware(NetworkGraph.store),
  499. storeActions: NetworkGraph.createActions(),
  500. }),
  501. document.getElementById(HTML_ID_REF_GRAPH)
  502. );