// jQuery UI style "widget" for displaying a graph //////////////////////////////////////////////////////////////////////////////// // // graph // $(function() { // the widget definition, where "itop" is the namespace, // "dashboard" the widget name $.widget( "itop.simple_graph", { // default options options: { align: 'center', 'vertical-align': 'middle', source_url: null, sources: {}, excluded: {}, export_as_pdf: null, page_format: { label: 'Page Format:', values: { A3: 'A3', A4: 'A4', Letter: 'Letter' }, 'default': 'A4'}, page_orientation: { label: 'Page Orientation:', values: { P: 'Portait', L: 'Landscape' }, 'default': 'L' }, labels: { export_pdf_title: 'PDF Export Options', cancel: 'Cancel', 'export': 'Export', title: 'Document Title', include_list: 'Include the list of objects', comments: 'Comments', grouping_threshold: 'Grouping Threshold', additional_context_info: 'Additional Context Info', refresh: 'Refresh', check_all: 'Check All', uncheck_all: 'Uncheck All', none_selected: 'None', nb_selected: '# selected', zoom: 'Zoom', loading: 'Loading...' }, export_as_document: null, drill_down: null, grouping_threshold: 10, excluded_classes: [], attachment_obj_class: null, attachment_obj_key: null, additional_contexts: [], context_key: '' }, // the constructor _create: function() { var me = this; this.aNodes = []; this.aEdges = []; this.fZoom = 1.0; this.xOffset = 0; this.yOffset = 0; this.xPan = 0; this.yPan = 0; this.iTextHeight = 12; this.fSliderZoom = 1.0; this.bInUpdateSliderZoom = false; this.bRedrawNeeded = false; this.oPaper = Raphael(this.element.get(0), 16*this.element.width(), 16*this.element.height()); this.element .addClass('panel-resized') .addClass('itop-simple-graph') .addClass('graph'); this._create_toolkit_menu(); this._build_context_menus(); this.sTabId = null; var jTabPanel = this.element.closest('.ui-tabs-panel'); if (jTabPanel.length > 0) { // We are inside a tab, find out which one and hook its activation this.sTabId = jTabPanel.attr('id'); var jTabs = this.element.closest('.ui-tabs'); jTabs.on( "tabsactivate", function( event, ui ) { me._on_tabs_activate(ui); }); } $(window).bind('resized', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } ); $('#dh_flash').bind('toggle_complete', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } ); this.element.bind('mousewheel', function(event, delta, deltaX, deltaY) { return me._on_mousewheel(event, delta, deltaX, deltaY); }); if (this.options.source_url != null) { this.load_from_url(this.options.source_url); } }, // called when created, and later when changing options _refresh: function() { this.draw(); }, // events bound via _bind are removed automatically // revert other modifications here _destroy: function() { var sId = this.element.attr('id'); this.element .removeClass('itop-simple-graph') .removeClass('graph'); $('#tk_graph'+sId).remove(); $('#graph_'+sId+'_export_as_pdf').remove(); }, // _setOptions is called with a hash of all options that are changing _setOptions: function() { this._superApply(arguments); }, // _setOption is called for each individual option that is changing _setOption: function( key, value ) { this._superApply(arguments); }, draw: function() { this._updateBBox(); this.auto_scale(); this.oPaper.clear(); this._reset this.oPaper.setViewBox(this.xPan, this.yPan, this.element.width(), this.element.height(), false); for(var k in this.aNodes) { this.aNodes[k].aElements = []; this._draw_node(this.aNodes[k]); } for(var k in this.aEdges) { this.aEdges[k].aElements = []; this._draw_edge(this.aEdges[k]); } var me = this; this.oBackground = this.oPaper .rect(-10000, -10000, 20000, 20000) .attr({fill: '#fff', opacity: 0, cursor: 'move'}) .toBack() .drag(function(dx, dy, x, y, event) { me._on_background_move(dx, dy, x, y, event); }, function(x, y, event) { me._on_background_drag_start(x, y, event); }, function (event) { me._on_background_drag_end(event); }); this._make_tooltips(); }, _draw_node: function(oNode) { var iWidth = oNode.width; var iHeight = 32; var iFontSize = 10; var fTotalZoom = this.fZoom * this.fSliderZoom; var xPos = Math.round(oNode.x * fTotalZoom + this.xOffset); var yPos = Math.round(oNode.y * fTotalZoom + this.yOffset); oNode.tx = 0; oNode.ty = 0; switch(oNode.shape) { case 'disc': oScaledAttr = {}; for(k in oNode.disc_attr) { value = oNode.disc_attr[k] switch(k) { // Scalable attributes case 'stroke-width': value = value * fTotalZoom; break; } oScaledAttr[k] = value; } oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*fTotalZoom / 2).attr(oScaledAttr)); var oText = this.oPaper.text(xPos, yPos, oNode.label); oNode.text_attr['font-size'] = iFontSize * fTotalZoom; oText.attr(oNode.text_attr); //oText.transform('s'+this.fZoom); oNode.aElements.push(oText); break; case 'group': oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*fTotalZoom / 2).attr({fill: '#fff', 'stroke-width':0})); oScaledAttr = {}; for(k in oNode.disc_attr) { value = oNode.disc_attr[k] switch(k) { // Scalable attributes case 'stroke-width': value = value * fTotalZoom; break; } oScaledAttr[k] = value; } oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*fTotalZoom / 2).attr(oScaledAttr)); var xIcon = xPos - 18 * fTotalZoom; var yIcon = yPos - 18 * fTotalZoom; oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon, yIcon, 16*fTotalZoom, 16*fTotalZoom).attr(oNode.icon_attr)); oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*fTotalZoom, yIcon, 16*fTotalZoom, 16*fTotalZoom).attr(oNode.icon_attr)); oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 9*fTotalZoom, yIcon + 18*fTotalZoom, 16*fTotalZoom, 16*fTotalZoom).attr(oNode.icon_attr)); var oText = this.oPaper.text(xPos, yPos +2, oNode.label); oNode.text_attr['font-size'] = iFontSize * fTotalZoom; oText.attr(oNode.text_attr); //oText.transform('s'+this.fZoom); var oBB = oText.getBBox(); var dy = iHeight/2*fTotalZoom + oBB.height/2; oText.remove(); oText = this.oPaper.text(xPos, yPos +dy +2, oNode.label); oText.attr(oNode.text_attr); //oText.transform('s'+this.fZoom); oNode.aElements.push(oText); 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})); oText.toFront(); break; case 'icon': if(Raphael.svg) { // the colorShift plugin works only in SVG oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * fTotalZoom/2, yPos - iHeight * fTotalZoom/2, iWidth*fTotalZoom, iHeight*fTotalZoom).colorShift('#fff', 1)); } oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * fTotalZoom/2, yPos - iHeight * fTotalZoom/2, iWidth*fTotalZoom, iHeight*fTotalZoom).attr(oNode.icon_attr)); var idx = 0; for(var i in oNode.context_icons) { var sgn = 2*(idx % 2) -1; // Suite: -1, 1, -1, 1, -1, 1, -1, etc. var coef = Math.floor((1+idx)/2) * sgn; // Suite: 0, 1, -1, 2, -2, 3, -3, etc. var alpha = coef*Math.PI/4 - Math.PI/2; var x = xPos + Math.cos(alpha) * 1.25*iWidth * fTotalZoom / 2; var y = yPos + Math.sin(alpha) * 1.25*iWidth * fTotalZoom / 2; var l = iWidth/3 * fTotalZoom; oNode.aElements.push(this.oPaper.image(oNode.context_icons[i], x - l/2, y - l/2, l , l).attr(oNode.icon_attr)); idx++; } var oText = this.oPaper.text( xPos, yPos, oNode.label); oNode.text_attr['font-size'] = iFontSize * fTotalZoom; oText.attr(oNode.text_attr); //oText.transform('S'+fTotalZoom); var oBB = oText.getBBox(); var dy = iHeight/2*fTotalZoom + oBB.height/2; oText.remove(); oText = this.oPaper.text( xPos, yPos + dy, oNode.label); oText.attr(oNode.text_attr); //oText.transform('S'+fTotalZoom); oNode.aElements.push(oText); 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()); break; } if (oNode.source) { oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*fTotalZoom / 2).attr({stroke: '#c33', 'stroke-width': 3*fTotalZoom }).toBack()); } if (oNode.sink) { oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*fTotalZoom / 2).attr({stroke: '#33c', 'stroke-width': 3*fTotalZoom }).toBack()); } var me = this; for(k in oNode.aElements) { var sNodeId = oNode.id; $(oNode.aElements[k].node).attr({'data-type': oNode.shape, 'data-id': oNode.id} ).attr('class', 'popupMenuTarget'); oNode.aElements[k].drag( function(dx, dy, x, y, event) { clearTimeout($(this.node).data('openTimeoutId')); 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); } ); } }, _move: function(sNodeId, dx, dy, x, y, event) { var fTotalZoom = this.fZoom * this.fSliderZoom; var origDx = dx / fTotalZoom; var origDy = dy / fTotalZoom; var oNode = this._find_node(sNodeId); oNode.x = oNode.xOrig + origDx; oNode.y = oNode.yOrig + origDy; for(k in oNode.aElements) { oNode.aElements[k].transform('t'+(oNode.tx + dx)+', '+(oNode.ty + dy)); for(j in this.aEdges) { var oEdge = this.aEdges[j]; if ((oEdge.source_node_id == sNodeId) || (oEdge.sink_node_id == sNodeId)) { var sPath = this._get_edge_path(oEdge); oEdge.aElements[0].attr({path: sPath}); } } } }, _drag_start: function(sNodeId, x, y, event) { var oNode = this._find_node(sNodeId); oNode.xOrig = oNode.x; oNode.yOrig = oNode.y; }, _drag_end: function(sNodeId, event) { var fTotalZoom = this.fZoom * this.fSliderZoom; var oNode = this._find_node(sNodeId); oNode.tx += (oNode.x - oNode.xOrig) * fTotalZoom; oNode.ty += (oNode.y - oNode.yOrig) * fTotalZoom; oNode.xOrig = oNode.x; oNode.yOrig = oNode.y; this._updateBBox(); }, _updateBBox: function() { this.options.xmin = 9999; this.options.xmax = -9999; this.options.ymin = 9999; this.options.ymax = -9999; for(var k in this.aNodes) { this.options.xmin = Math.min(this.aNodes[k].x + this.aNodes[k].tx - this.aNodes[k].width/2, this.options.xmin); this.options.xmax = Math.max(this.aNodes[k].x + this.aNodes[k].tx + this.aNodes[k].width/2, this.options.xmax); this.options.ymin = Math.min(this.aNodes[k].y + this.aNodes[k].ty - this.aNodes[k].width/2, this.options.ymin); this.options.ymax = Math.max(this.aNodes[k].y + this.aNodes[k].ty + this.aNodes[k].width/2, this.options.ymax); } }, _get_edge_path: function(oEdge) { var fTotalZoom = this.fZoom * this.fSliderZoom; var oStart = this._find_node(oEdge.source_node_id); var oEnd = this._find_node(oEdge.sink_node_id); var iArrowSize = 5; if ((oStart == null) || (oEnd == null)) return ''; var xStart = Math.round(oStart.x * fTotalZoom + this.xOffset); var yStart = Math.round(oStart.y * fTotalZoom + this.yOffset); var xEnd = Math.round(oEnd.x * fTotalZoom + this.xOffset); var yEnd = Math.round(oEnd.y * fTotalZoom + this.yOffset); var sPath = Raphael.format('M{0},{1}L{2},{3}', xStart, yStart, xEnd, yEnd); var vx = (xEnd - xStart); var vy = (yEnd - yStart); var l = Math.sqrt(vx*vx+vy*vy); vx = vx / l; vy = vy / l; var ux = -vy; var uy = vx; var lPos = Math.max(l/2, l - 40*fTotalZoom); var xArrow = xStart + vx * lPos; var yArrow = yStart + vy * lPos; sPath += Raphael.format('M{0},{1}l{2},{3}M{4},{5}l{6},{7}', xArrow, yArrow, fTotalZoom * iArrowSize *(-vx + ux), fTotalZoom * iArrowSize *(-vy + uy), xArrow, yArrow, fTotalZoom * iArrowSize *(-vx - ux), fTotalZoom * iArrowSize *(-vy - uy)); return sPath; }, _draw_edge: function(oEdge) { var fTotalZoom = this.fZoom * this.fSliderZoom; var fStrokeSize = Math.max(1, 2 * fTotalZoom); var sPath = this._get_edge_path(oEdge); var oAttr = $.extend(oEdge.attr); oAttr['stroke-linecap'] = 'round'; oAttr['stroke-width'] = fStrokeSize; oEdge.aElements.push(this.oPaper.path(sPath).attr(oAttr).toBack()); }, _find_node: function(sId) { for(var k in this.aNodes) { if (this.aNodes[k].id == sId) return this.aNodes[k]; } return null; }, adjust_height: function() { var maxHeight = this.element.parent().height(); // Compute the available height var element = this.element; this.element.parent().children().each(function() { if($(this).is(':visible') && !$(this).hasClass('graph') && ($(this).attr('id') != element.attr('id'))) { maxHeight = maxHeight - $(this).height(); } }); this.element.height(maxHeight - 20); this.oPaper.setSize(this.element.width(), this.element.height()); }, auto_scale: function() { var fMaxZoom = 1.5; this.adjust_height(); iMargin = 10; xmin = this.options.xmin - iMargin; xmax = this.options.xmax + iMargin; ymin = this.options.ymin - iMargin; ymax = this.options.ymax + iMargin; var xScale = this.element.width() / (xmax - xmin); var yScale = this.element.height() / (ymax - ymin + this.iTextHeight); this.fZoom = Math.min(xScale, yScale, fMaxZoom); switch(this.options.align) { case 'left': this.xOffset = -xmin * this.fZoom; break; case 'right': this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom); break; case 'center': this.xOffset = -xmin * this.fZoom + (this.element.width() - (xmax - xmin) * this.fZoom) / 2; break; } switch(this.options['vertical-align']) { case 'top': this.yOffset = -ymin * this.fZoom; break; case 'bottom': this.yOffset = this.element.height() - (ymax + this.iTextHeight) * this.fZoom; break; case 'middle': this.yOffset = -ymin * this.fZoom + (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2; break; } }, add_node: function(oNode) { oNode.aElements = []; oNode.tx = 0; oNode.ty = 0; this.aNodes.push(oNode); }, add_edge: function(oEdge) { oEdge.aElements = []; this.aEdges.push(oEdge); }, show_group: function(sGroupId) { this._close_all_tooltips(); // Activate the 3rd tab this.element.closest('.ui-tabs').tabs("option", "active", 2); // Scroll into view the group if ($('#'+sGroupId).length > 0) { $('#'+sGroupId)[0].scrollIntoView(); } }, _create_toolkit_menu: function() { var sPopupMenuId = 'tk_graph'+this.element.attr('id'); var sHtml = '