simple_graph.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. // jQuery UI style "widget" for displaying a graph
  2. ////////////////////////////////////////////////////////////////////////////////
  3. //
  4. // graph
  5. //
  6. $(function()
  7. {
  8. // the widget definition, where "itop" is the namespace,
  9. // "dashboard" the widget name
  10. $.widget( "itop.simple_graph",
  11. {
  12. // default options
  13. options:
  14. {
  15. xmin: 0,
  16. xmax: 0,
  17. ymin: 0,
  18. ymax: 0,
  19. align: 'center',
  20. 'vertical-align': 'middle',
  21. export_as_pdf_url: '',
  22. export_as_document_url: '',
  23. drill_down_url: '',
  24. },
  25. // the constructor
  26. _create: function()
  27. {
  28. var me = this;
  29. this.aNodes = [];
  30. this.aEdges = [];
  31. this.fZoom = 1.0;
  32. this.xOffset = 0;
  33. this.yOffset = 0;
  34. this.iTextHeight = 12;
  35. this.auto_scale();
  36. this.oPaper = Raphael(this.element.get(0), this.element.width(), this.element.height());
  37. this.element
  38. .addClass('itop-simple-graph')
  39. .addClass('graph');
  40. this._create_toolkit_menu();
  41. this._build_context_menus();
  42. },
  43. // called when created, and later when changing options
  44. _refresh: function()
  45. {
  46. this.draw();
  47. },
  48. // events bound via _bind are removed automatically
  49. // revert other modifications here
  50. _destroy: function()
  51. {
  52. var sId = this.element.attr('id');
  53. this.element
  54. .removeClass('itop-simple-graph')
  55. .removeClass('graph');
  56. $('#tk_graph'+sId).remove();
  57. $('#graph_'+sId+'_export_as_pdf').remove();
  58. },
  59. // _setOptions is called with a hash of all options that are changing
  60. _setOptions: function()
  61. {
  62. this._superApply(arguments);
  63. },
  64. // _setOption is called for each individual option that is changing
  65. _setOption: function( key, value )
  66. {
  67. this._superApply(arguments);
  68. },
  69. draw: function()
  70. {
  71. this.oPaper.clear();
  72. for(var k in this.aNodes)
  73. {
  74. this._draw_node(this.aNodes[k]);
  75. }
  76. for(var k in this.aEdges)
  77. {
  78. this._draw_edge(this.aEdges[k]);
  79. }
  80. },
  81. _draw_node: function(oNode)
  82. {
  83. var iWidth = oNode.width;
  84. var iHeight = 32;
  85. var xPos = Math.round(oNode.x * this.fZoom + this.xOffset);
  86. var yPos = Math.round(oNode.y * this.fZoom + this.yOffset);
  87. oNode.tx = 0;
  88. oNode.ty = 0;
  89. switch(oNode.shape)
  90. {
  91. case 'disc':
  92. oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
  93. var oText = this.oPaper.text(xPos, yPos, oNode.label);
  94. oText.attr(oNode.text_attr);
  95. oText.transform('s'+this.fZoom);
  96. oNode.aElements.push(oText);
  97. break;
  98. case 'group':
  99. oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr({fill: '#fff', 'stroke-width':0}));
  100. oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
  101. var xIcon = xPos - 18 * this.fZoom;
  102. var yIcon = yPos - 18 * this.fZoom;
  103. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
  104. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*this.fZoom, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
  105. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 9*this.fZoom, yIcon + 18*this.fZoom, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
  106. var oText = this.oPaper.text(xPos, yPos +2, oNode.label);
  107. oText.attr(oNode.text_attr);
  108. oText.transform('s'+this.fZoom);
  109. var oBB = oText.getBBox();
  110. var dy = iHeight/2*this.fZoom + oBB.height/2;
  111. oText.remove();
  112. oText = this.oPaper.text(xPos, yPos +dy +2, oNode.label);
  113. oText.attr(oNode.text_attr);
  114. oText.transform('s'+this.fZoom);
  115. oNode.aElements.push(oText);
  116. oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}));
  117. oText.toFront();
  118. break;
  119. case 'icon':
  120. if(Raphael.svg)
  121. {
  122. // the colorShift plugin works only in SVG
  123. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).colorShift('#fff', 1));
  124. }
  125. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).attr(oNode.icon_attr));
  126. var oText = this.oPaper.text( xPos, yPos, oNode.label);
  127. oText.attr(oNode.text_attr);
  128. oText.transform('S'+this.fZoom);
  129. var oBB = oText.getBBox();
  130. var dy = iHeight/2*this.fZoom + oBB.height/2;
  131. oText.remove();
  132. oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
  133. oText.attr(oNode.text_attr);
  134. oText.transform('S'+this.fZoom);
  135. oNode.aElements.push(oText);
  136. oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}).toBack());
  137. break;
  138. }
  139. if (oNode.source)
  140. {
  141. oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*this.fZoom / 2).attr({stroke: '#c33', 'stroke-width': 3*this.fZoom }).toBack());
  142. }
  143. if (oNode.sink)
  144. {
  145. oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*this.fZoom / 2).attr({stroke: '#33c', 'stroke-width': 3*this.fZoom }).toBack());
  146. }
  147. var me = this;
  148. for(k in oNode.aElements)
  149. {
  150. var sNodeId = oNode.id;
  151. $(oNode.aElements[k].node).attr({'data-type': oNode.shape, 'data-id': oNode.id} ).attr('class', 'popupMenuTarget');
  152. oNode.aElements[k].drag(function(dx, dy, x, y, event) { me._move(sNodeId, dx, dy, x, y, event); }, function(x, y, event) { me._drag_start(sNodeId, x, y, event); }, function (event) { me._drag_end(sNodeId, event); });
  153. }
  154. },
  155. _move: function(sNodeId, dx, dy, x, y, event)
  156. {
  157. var origDx = dx / this.fZoom;
  158. var origDy = dy / this.fZoom;
  159. var oNode = this._find_node(sNodeId);
  160. oNode.x = oNode.xOrig + origDx;
  161. oNode.y = oNode.yOrig + origDy;
  162. for(k in oNode.aElements)
  163. {
  164. oNode.aElements[k].transform('t'+(oNode.tx + dx)+', '+(oNode.ty + dy));
  165. for(j in this.aEdges)
  166. {
  167. var oEdge = this.aEdges[j];
  168. if ((oEdge.source_node_id == sNodeId) || (oEdge.sink_node_id == sNodeId))
  169. {
  170. var sPath = this._get_edge_path(oEdge);
  171. oEdge.aElements[0].attr({path: sPath});
  172. }
  173. }
  174. }
  175. },
  176. _drag_start: function(sNodeId, x, y, event)
  177. {
  178. var oNode = this._find_node(sNodeId);
  179. oNode.xOrig = oNode.x;
  180. oNode.yOrig = oNode.y;
  181. },
  182. _drag_end: function(sNodeId, event)
  183. {
  184. var oNode = this._find_node(sNodeId);
  185. oNode.tx += (oNode.x - oNode.xOrig) * this.fZoom;
  186. oNode.ty += (oNode.y - oNode.yOrig) * this.fZoom;
  187. oNode.xOrig = oNode.x;
  188. oNode.yOrig = oNode.y;
  189. },
  190. _get_edge_path: function(oEdge)
  191. {
  192. var oStart = this._find_node(oEdge.source_node_id);
  193. var oEnd = this._find_node(oEdge.sink_node_id);
  194. var iArrowSize = 5;
  195. if ((oStart == null) || (oEnd == null)) return '';
  196. var xStart = Math.round(oStart.x * this.fZoom + this.xOffset);
  197. var yStart = Math.round(oStart.y * this.fZoom + this.yOffset);
  198. var xEnd = Math.round(oEnd.x * this.fZoom + this.xOffset);
  199. var yEnd = Math.round(oEnd.y * this.fZoom + this.yOffset);
  200. var sPath = Raphael.format('M{0},{1}L{2},{3}', xStart, yStart, xEnd, yEnd);
  201. var vx = (xEnd - xStart);
  202. var vy = (yEnd - yStart);
  203. var l = Math.sqrt(vx*vx+vy*vy);
  204. vx = vx / l;
  205. vy = vy / l;
  206. var ux = -vy;
  207. var uy = vx;
  208. var lPos = Math.max(l/2, l - 40*this.fZoom);
  209. var xArrow = xStart + vx * lPos;
  210. var yArrow = yStart + vy * lPos;
  211. sPath += Raphael.format('M{0},{1}l{2},{3}M{4},{5}l{6},{7}', xArrow, yArrow, this.fZoom * iArrowSize *(-vx + ux), this.fZoom * iArrowSize *(-vy + uy), xArrow, yArrow, this.fZoom * iArrowSize *(-vx - ux), this.fZoom * iArrowSize *(-vy - uy));
  212. return sPath;
  213. },
  214. _draw_edge: function(oEdge)
  215. {
  216. var fStrokeSize = Math.max(1, 2 * this.fZoom);
  217. var sPath = this._get_edge_path(oEdge);
  218. var oAttr = $.extend(oEdge.attr);
  219. oAttr['stroke-linecap'] = 'round';
  220. oAttr['stroke-width'] = fStrokeSize;
  221. oEdge.aElements.push(this.oPaper.path(sPath).attr(oAttr).toBack());
  222. },
  223. _find_node: function(sId)
  224. {
  225. for(var k in this.aNodes)
  226. {
  227. if (this.aNodes[k].id == sId) return this.aNodes[k];
  228. }
  229. return null;
  230. },
  231. auto_scale: function()
  232. {
  233. var fMaxZoom = 1.5;
  234. var maxHeight = this.element.parent().height();
  235. // Compute the available height
  236. var element = this.element;
  237. this.element.parent().children().each(function() {
  238. if($(this).is(':visible') && !$(this).hasClass('graph') && ($(this).attr('id') != element.attr('id')))
  239. {
  240. maxHeight = maxHeight - $(this).height();
  241. }
  242. });
  243. this.element.height(maxHeight - 20);
  244. iMargin = 10;
  245. xmin = this.options.xmin - iMargin;
  246. xmax = this.options.xmax + iMargin;
  247. ymin = this.options.ymin - iMargin;
  248. ymax = this.options.ymax + iMargin;
  249. var xScale = this.element.width() / (xmax - xmin);
  250. var yScale = this.element.height() / (ymax - ymin + this.iTextHeight);
  251. this.fZoom = Math.min(xScale, yScale, fMaxZoom);
  252. switch(this.options.align)
  253. {
  254. case 'left':
  255. this.xOffset = -xmin * this.fZoom;
  256. break;
  257. case 'right':
  258. this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom);
  259. break;
  260. case 'center':
  261. this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
  262. break;
  263. }
  264. switch(this.options['vertical-align'])
  265. {
  266. case 'top':
  267. this.yOffset = -ymin * this.fZoom;
  268. break;
  269. case 'bottom':
  270. this.yOffset = this.element.height() - (ymax + this.iTextHeight) * this.fZoom;
  271. break;
  272. case 'middle':
  273. this.yOffset = (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
  274. break;
  275. }
  276. },
  277. add_node: function(oNode)
  278. {
  279. oNode.aElements = [];
  280. this.aNodes.push(oNode);
  281. },
  282. add_edge: function(oEdge)
  283. {
  284. oEdge.aElements = [];
  285. this.aEdges.push(oEdge);
  286. },
  287. show_group: function(sGroupId)
  288. {
  289. // Activate the 3rd tab
  290. this.element.closest('.ui-tabs').tabs("option", "active", 2);
  291. // Scroll into view the group
  292. if ($('#'+sGroupId).length > 0)
  293. {
  294. $('#'+sGroupId)[0].scrollIntoView();
  295. }
  296. },
  297. _create_toolkit_menu: function()
  298. {
  299. var sPopupMenuId = 'tk_graph'+this.element.attr('id');
  300. var sHtml = '<div class="itop_popup toolkit_menu graph" style="font-size: 12px;" id="'+sPopupMenuId+'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
  301. if (this.options.export_as_pdf_url != '')
  302. {
  303. sHtml += '<li><a href="#" id="'+sPopupMenuId+'_pdf">Export as PDF...</a></li>';
  304. }
  305. if (this.options.export_as_document_url != '')
  306. {
  307. sHtml += '<li><a href="#" id="'+sPopupMenuId+'_document">Export as document...</a></li>';
  308. }
  309. sHtml += '<li><a href="#" id="'+sPopupMenuId+'_reload">Refresh</a></li>';
  310. sHtml += '</ul></li></ul></div>';
  311. this.element.before(sHtml);
  312. $('#'+sPopupMenuId).popupmenu();
  313. var me = this;
  314. $('#'+sPopupMenuId+'_pdf').click(function() { me.export_as_pdf(); });
  315. $('#'+sPopupMenuId+'_document').click(function() { me.export_as_document(); });
  316. $('#'+sPopupMenuId+'_reload').click(function() { me.reload(); });
  317. },
  318. _build_context_menus: function()
  319. {
  320. var sId = this.element.attr('id');
  321. var me = this;
  322. $.contextMenu({
  323. selector: '#'+sId+' .popupMenuTarget',
  324. build: function(trigger, e) {
  325. // this callback is executed every time the menu is to be shown
  326. // its results are destroyed every time the menu is hidden
  327. // e is the original contextmenu event, containing e.pageX and e.pageY (amongst other data)
  328. var sType = trigger.attr('data-type');
  329. var sNodeId = trigger.attr('data-id');
  330. var oNode = me._find_node(sNodeId);
  331. /*
  332. var sObjName = trigger.attr('data-class');
  333. var sIndex = trigger.attr('data-index');
  334. var originalEvent = e;
  335. var bHasItems = false;
  336. */
  337. var oResult = {callback: null, items: {}};
  338. switch(sType)
  339. {
  340. case 'group':
  341. var sGroupIndex = oNode.group_index;
  342. oResult = {
  343. callback: function(key, options) {
  344. var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
  345. me.show_group('relation_group_'+sGroupIndex);
  346. },
  347. items: { 'show': {name: 'Show group' } }
  348. };
  349. break;
  350. case 'icon':
  351. var sObjClass = oNode.obj_class;
  352. var sObjKey = oNode.obj_key;
  353. oResult = {
  354. callback: function(key, options) {
  355. var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
  356. var sURL = me.options.drill_down_url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
  357. window.location.href = sURL;
  358. },
  359. items: { 'details': {name: 'Show Details' } }
  360. };
  361. break;
  362. default:
  363. oResult = false; // No context menu
  364. }
  365. return oResult;
  366. }
  367. });
  368. },
  369. export_as_pdf: function()
  370. {
  371. var oPositions = {};
  372. for(k in this.aNodes)
  373. {
  374. oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
  375. }
  376. var sHtmlForm = '<div id="PDFExportDlg'+this.element.attr('id')+'"><form id="graph_'+this.element.attr('id')+'_export_as_pdf" target="_blank" action="'+this.options.export_as_pdf_url+'" method="post">';
  377. sHtmlForm += '<input type="hidden" name="positions" value="">';
  378. sHtmlForm += '<table>';
  379. sHtmlForm += '<tr><td>Page format:</td><td><select name="p"><option value="A3">A3</option><option value="A4" selected>A4</option><option value="Letter">Letter</option></select></td></tr>';
  380. sHtmlForm += '<tr><td>Page orientation:</td><td><select name="o"><option value="L" selected>Landscape</option><option value="P">Portrait</select></td></tr>';
  381. sHtmlForm += '<table>';
  382. sHtmlForm += '</form></div>';
  383. $('body').append(sHtmlForm);
  384. $('#graph_'+this.element.attr('id')+'_export_as_pdf input[name="positions"]').val(JSON.stringify(oPositions));
  385. var me = this;
  386. $('#PDFExportDlg'+this.element.attr('id')).dialog({
  387. modal: true,
  388. title: 'PDF format options',
  389. buttons: [
  390. {text: 'Cancel', click: function() { $(this).dialog('close');} },
  391. {text: 'Export', click: function() { $('#graph_'+me.element.attr('id')+'_export_as_pdf').submit(); $(this).dialog('close');} },
  392. ]
  393. });
  394. //$('#graph_'+this.element.attr('id')+'_export_as_pdf').submit();
  395. },
  396. export_as_document: function()
  397. {
  398. alert('Export as document: not yet implemented');
  399. },
  400. reload: function()
  401. {
  402. alert('Reload: not yet implemented');
  403. }
  404. });
  405. });