Org_Chart.js

This project was developed as a modular web-based charting/graphing application similar to MS Visio. It is capable of importing, modifying and exporting chart/graph diagrams as HTML markup readily displayed on a webpage. PHP scripts were called with AJAX to read and write the HTML markup to a separate file on the server.

The application was written in Vanilla JavaScript following a modular OOP prototype pattern making it possible for more advanced elements, features and interface elements to be implemented in the future.

Only part of this application, specifying the structure of objects representing the graph elements, is shown below. The administrative interface, import/export and display features were implemented separately. A live demo will hopefully be demonstrated soon.

/******************************************************************************/
/*  This class describes the structure of an Org Chart.                       */
/******************************************************************************/
function Org_Chart(init_options) {
	this.uri;
	
	//Elements and Links... (side thought - do they have to be different?)
	this.graph_elements = [];
	this.graph_links = [];
	
	this.page_element_id = init_options.page_element_id;
	this.page_element = document.getElementById(this.page_element_id);
	this.page_links;
	
	this.page_element.style.position = 'relative';
	this.page_element.style.background = init_options.background;
	this.page_element.style.border = init_options.border;
	this.page_element.style.width = init_options.width;
	this.page_element.style.height = init_options.height;
	this.page_element.style.overflow = 'hidden';
	
	this.default_node = {
		'border': '1px dotted #222222', 
		'background': '#eeeeee', 
		'width': '100px', 
		'height': '50px', 
		'padding': '10px', 
		'z-index': '20', 
		'textAlign': 'center', 
		'zIndex': 100 };
	this.grid_spacing = init_options.grid_spacing;
	
	//All the control objects belonging to this element
	this.controls = new Object();
	
	this.appendControls = function(element) {
		for( c_i in element.controls ) {
			if(element.controls[c_i].control_element) {
				element.getElement().appendChild(element.controls[c_i].control_element);
			}
		}
	}
	
	//Draw Function
	this.draw_graph = function() { 
		for( var element_index = 0; element_index < this.graph_elements.length; element_index++ ) { 
			this.page_element.appendChild( this.graph_elements[element_index].getElement() ); 
		} 
	};
	
	this.getElement = function() {
		return this.page_element;
	}
}
//Cross browser add event listener function
Org_Chart.prototype.addCrossListener = function(eventTarget, eventName, eventFunction, eventBubble) {
	if(eventTarget.addEventListener != null) {
		eventTarget.addEventListener(eventName, eventFunction, eventBubble);
	} else {
		eventTarget.attachEvent('on' + eventName, eventFunction);
	}
};
//Cross browser remove event listener function
Org_Chart.prototype.removeCrossListener = function(eventTarget, eventName, eventFunction, eventBubble) {
	if(eventTarget.removeEventListener != null) {
		eventTarget.removeEventListener(eventName, eventFunction, eventBubble);
	} else {
		eventTarget.removeEvent('on' + eventName, eventFunction);
	}
};

/******************************************************************************/
/*  This class describes the structure of an Org Chart element/node.          */
/******************************************************************************/
function Org_Chart_Element(org_instance, type, style_options, content) {
	//Properties of this element
	this.type = type;
	this.original_style = style_options;
	this.content = document.createElement('div');
	this.content.innerHTML = content;
	this.content.className = 'element_content';
	//this.content.style.textAlign = style_options;
	
	//All your controls object are belong to this
	this.controls = new Object();
	
	//Background of this element
	this.parents = [];
	this.children = [];
	this.links = [];
	
	this.element = document.createElement('div');
	this.org_instance = org_instance;
	this.container = org_instance.page_element;
	
	//Add all the content & style options 
	(typeof content !== 'undefined') ? (this.element.appendChild(this.content)) : false ;
	this.element.className = 'org_chart_element org_chart_element-' + this.type;
	this.element.style.position = 'absolute';
	
	for( o_i in style_options )
	{
		this.element.style[o_i] = style_options[o_i];
	}
	
	this.appendControls = function(element) {
		for( c_i in element.controls ){
			if(element.controls[c_i].control_element) {
				element.getElement().appendChild(element.controls[c_i].control_element);
			}
		}
	}
	
	//Function to return a reference to the DOM element
	this.getElement = function() { return this.element; };
	
	//Function to generate JSON of this 
	this.getJSON = function() {
		var element_style = {
			'width': this.element.style.width, 
			'height': this.element.style.height, 
			'top': this.element.style.top, 
			'left': this.element.style.left, 
			'position': this.element.style.position, 
			'background': this.element.style.background, 
			'border': this.element.style.border, 
			'padding': this.element.style.padding, 
			'z-index': this.element.style['z-index']
		};
		var content_style = {
			'width': this.content.style.width, 
			'height': this.content.style.height, 
			'top': this.content.style.top, 
			'left': this.content.style.left, 
			'position': this.content.style.position, 
			'background': this.content.style.background, 
			'border': this.content.style.border, 
			'text-align': 'center' 
		};
		var element_JSON = { 
			'element_style': element_style, 
			'content_style': content_style, 
			'content': this.content.innerHTML
		};
		
		return element_JSON;
	}
	
	this.setJSON = function(JSON_data) {
		var style_targets = ['width', 'height', 'position', 'top', 'left', 'border', 'background', 'z-index', 'padding', 'text-align'];
		
		for(var e_style in style_targets) {
			this.element.style[style_targets[e_style]] = JSON_data.element_style[style_targets[e_style]];
		}
		
		this.element.style.textAlign = JSON_data.content_style['text-align'];
		
		this.content.innerHTML = JSON_data.content;
		
		return true;
	}
}



/******************************************************************************/
/*  This class describes the structure of an Org Chart link.                  */
/******************************************************************************/
function Org_Chart_Link(org_instance, type, style_options) {
	this.type = type;
	this.org_instance = org_instance;
	this.container = org_instance.page_element;
	
	this.element = document.createElement('div');
	this.controls = new Object();
	
	//Line segment collection and its three lines
	this.line_segments = [];
	
	for( var i = 0; i < 3; i++) {
		var new_line_segment = document.createElement('div');
		new_line_segment.style.background = 'black';
		new_line_segment.style.width = '1px';
		new_line_segment.style.height = '1px';
		new_line_segment.className = 'org_chart-link_segment';
		
		this.line_segments.push(new_line_segment);
		this.element.appendChild(new_line_segment);
	}
	
	//Add all the content & style options 
	this.element.className = 'org_chart_link org_chart_link-' + this.type;
	this.element.style.position = 'absolute';	
	
	for( o_i in style_options ) {
		this.element.style[o_i] = style_options[o_i];
	}
	
	this.appendControls = function(element) {
		for( c_i in element.controls ) {
			if(element.controls[c_i].control_element) {
				element.getElement().appendChild(element.controls[c_i].control_element);
			}
		}
	};
	
	//The respective nodes, and the offset relative to them
	this.start_node;
	this.end_node;
	this.element_focus;
	
	this.start_node_face;
	this.start_node_offsetLeft;
	this.start_node_offsetTop;
	
	this.end_node_face;
	this.end_node_offsetLeft;
	this.end_node_offsetTop;
	//Function to return a reference to the DOM element
	this.getElement = function() { return this.element; };
	
	//Function to generate JSON of this 
	this.getJSON = function() {
		var element_style = {
			'width': this.element.style.width, 
			'height': this.element.style.height, 
			'top': this.element.style.top, 
			'left': this.element.style.left, 
			'position': this.element.style.position, 
			'background': this.element.style.background, 
			'z-index': this.element.style['z-index'] };
		var s1_style = {
			'width': this.line_segments[0].style.width, 
			'height': this.line_segments[0].style.height, 
			'top': this.line_segments[0].style.top, 
			'left': this.line_segments[0].style.left, 
			'position': this.line_segments[0].style.position, 
			'background': this.line_segments[0].style.background, 
			'z-index': this.line_segments[0].style['z-index']};
		var s2_style = {
			'width': this.line_segments[1].style.width, 
			'height': this.line_segments[1].style.height, 
			'top': this.line_segments[1].style.top, 
			'left': this.line_segments[1].style.left, 
			'position': this.line_segments[1].style.position, 
			'background': this.line_segments[1].style.background, 
			'z-index': this.line_segments[1].style['z-index']};
		var s3_style = {
			'width': this.line_segments[2].style.width, 
			'height': this.line_segments[2].style.height, 
			'top': this.line_segments[2].style.top, 
			'left': this.line_segments[2].style.left, 
			'position': this.line_segments[2].style.position, 
			'background': this.line_segments[2].style.background, 
			'z-index': this.line_segments[2].style['z-index']};
		
		var link_JSON = { 
			'element_style': element_style, 
			'segment_1_style': s1_style, 
			'segment_2_style': s2_style, 
			'segment_3_style': s3_style};
											
		return link_JSON;
	}
	
	this.setJSON = function(JSON_data) {
		var style_targets = ['width', 'height', 'position', 'top', 'left', 'background', 'z-index'];
		
		for(var l_style in style_targets) {
			this.element.style[style_targets[l_style]] = JSON_data.element_style[style_targets[l_style]];
		}
		
		for(var l_style in style_targets) {
		{
			this.line_segments[2].style[style_targets[l_style]] = JSON_data.segment_1_style[style_targets[l_style]];
		}
		
		for(var l_style in style_targets) {
			this.line_segments[2].style[style_targets[l_style]] = JSON_data.segment_2_style[style_targets[l_style]];
		}
		
		for(var l_style in style_targets) {
			this.line_segments[2].style[style_targets[l_style]] = JSON_data.segment_3_style[style_targets[l_style]];
		}
		
		return true;
	}
	
	//Function to update the dimension of the div containing the link segments
	this.updateDimensions = function(element) {
		//Lets deal with resizing of the start node
		if( ( this.start_node_offsetTop > this.start_node.getElement().clientHeight && this.start_node_face == 'Bottom' ) || 
			( this.start_node_offsetTop < this.start_node.getElement().clientHeight && this.start_node_face == 'Bottom' ) )
			{
				this.start_node_offsetTop = this.start_node.getElement().clientHeight;
				
				if(this.start_node_offsetLeft > this.start_node.getElement().clientWidth)
					this.start_node_offsetLeft = this.start_node.getElement().clientWidth;
			}
		else if( (this.start_node_offsetLeft > this.start_node.getElement().clientWidth && this.start_node_face == 'Right' ) || 
				 ( this.start_node_offsetLeft < this.start_node.getElement().clientWidth && this.start_node_face == 'Right' ) )
			{
				this.start_node_offsetLeft = this.start_node.getElement().clientWidth;
				
				if(this.start_node_offsetTop > this.start_node.getElement().clientHeight)
					this.start_node_offsetTop = this.start_node.getElement().clientHeight;
			}
		
		//Lets deal with resizing of the end node
		if( ( this.end_node_offsetTop > this.end_node.getElement().clientHeight && this.end_node_face == 'Bottom' ) || 
			( this.end_node_offsetTop < this.end_node.getElement().clientHeight && this.end_node_face == 'Bottom' ) )
			{
				this.end_node_offsetTop = this.end_node.getElement().clientHeight;

				if(this.end_node_offsetLeft > this.end_node.getElement().clientWidth)
					this.end_node_offsetLeft = this.end_node.getElement().clientWidth;
			}
		else if( (this.end_node_offsetLeft > this.end_node.getElement().clientWidth && this.end_node_face == 'Right' ) || 
				 ( this.end_node_offsetLeft < this.end_node.getElement().clientWidth && this.end_node_face == 'Right' ) )
			{
				this.end_node_offsetLeft = this.end_node.getElement().clientWidth;
				
				if(this.end_node_offsetTop > this.end_node.getElement().clientHeight)
					this.end_node_offsetTop = this.end_node.getElement().clientHeight;
			}
			
		//Update the element on its Y axis
		if(this.start_node.getElement().offsetTop + this.start_node_offsetTop <= this.end_node.getElement().offsetTop + this.end_node_offsetTop)
		{
			this.element.style.top = this.start_node.getElement().offsetTop + this.start_node_offsetTop + 'px';
			this.element.style.height = (this.end_node.getElement().offsetTop + this.end_node_offsetTop) - (this.start_node.getElement().offsetTop + this.start_node_offsetTop) + 'px';
		}
		else
		{
			this.element.style.top = this.end_node.getElement().offsetTop + this.end_node_offsetTop + 'px';
			this.element.style.height = (this.start_node.getElement().offsetTop + this.start_node_offsetTop) - (this.end_node.getElement().offsetTop + this.end_node_offsetTop) + 'px';
		}
		
		//Update the element on its X axis
		if(this.start_node.getElement().offsetLeft + this.start_node_offsetLeft <= this.end_node.getElement().offsetLeft + this.end_node_offsetLeft)
		{
			this.element.style.left = this.start_node.getElement().offsetLeft + this.start_node_offsetLeft + 'px';
			this.element.style.width = (this.end_node.getElement().offsetLeft + this.end_node_offsetLeft) - (this.start_node.getElement().offsetLeft + this.start_node_offsetLeft) + 'px';
		}
		else
		{
			this.element.style.left = this.end_node.getElement().offsetLeft + this.end_node_offsetLeft + 'px';
			this.element.style.width = (this.start_node.getElement().offsetLeft + this.start_node_offsetLeft) - (this.end_node.getElement().offsetLeft + this.end_node_offsetLeft) + 'px';
		}
		
		this.updateLineSegments(this);
	}
	
	//Function to update the line segments making up this link
	this.updateLineSegments = function(link_element) {
		//First lets figure out if this link element has an end node, or if it is in creation
		var target_element = null;
		if(link_element.end_node != undefined) 
		{
			target_element = link_element.end_node;
			target_element.end_node_offsetLeft = link_element.end_node_offsetLeft;
			target_element.end_node_offsetTop = link_element.end_node_offsetTop;
		}
		else if(this.element_focus) 
		{
			target_element = this.element_focus;
			
			link_element.end_node_offsetLeft = target_element.grid_snap_handle.offsetLeft + target_element.grid_snap_handle.clientWidth/2;
			target_element.end_node_offsetLeft = target_element.grid_snap_handle.offsetLeft + target_element.grid_snap_handle.clientWidth/2;
			
			link_element.end_node_offsetTop = target_element.grid_snap_handle.offsetTop + target_element.grid_snap_handle.clientHeight/2;
			target_element.end_node_offsetTop = target_element.grid_snap_handle.offsetTop + target_element.grid_snap_handle.clientHeight/2;
		}
			
		if(link_element.start_node_face == 'Bottom') {
			//Dealing with the bottom edge, treat as flat bottom link
			//Default styles first
			link_element.line_segments[0].style.height = '100%';
			link_element.line_segments[0].style.width = '1px';
			link_element.line_segments[0].style.position = 'absolute';
			link_element.line_segments[0].style.top = '0px';
			link_element.line_segments[0].style.left = '0px';
			
			link_element.line_segments[1].style.height = '1px';
			link_element.line_segments[1].style.width = '100%';
			link_element.line_segments[1].style.position = 'absolute';
			link_element.line_segments[1].style.top = '100%';
			link_element.line_segments[1].style.left = '0px';
			
			link_element.line_segments[2].style.height = '0px';
			link_element.line_segments[2].style.width = '1px';
			link_element.line_segments[2].style.position = 'absolute';
			link_element.line_segments[2].style.top = '0px';
			link_element.line_segments[2].style.left = '100%';
			
			//Check if we are to the left of the origin (invert)
			if(link_element.getElement().offsetTop < link_element.start_node.getElement().offsetTop + link_element.start_node_offsetTop) {
				link_element.line_segments[1].style.top = '0px';
				link_element.line_segments[2].style.top = '0px';
			} else {
				link_element.line_segments[1].style.top = '100%';
				link_element.line_segments[2].style.top = '50%';
			}
			
			//Check if we are above the origin (invert)
			if(link_element.getElement().offsetLeft < link_element.start_node.getElement().offsetLeft + link_element.start_node_offsetLeft) {
				link_element.line_segments[0].style.left = '100%';
				link_element.line_segments[2].style.left = '0px';
			} else {
				link_element.line_segments[0].style.left = '0px';
				link_element.line_segments[2].style.left = '100%';
			}
			
			//Now target them snaps
			if( target_element && (target_element.end_node_offsetTop <= 0 || target_element.end_node_offsetTop >= target_element.getElement().clientHeight) )
			{
				link_element.line_segments[0].style.height = '50%';
				if(link_element.getElement().offsetTop < link_element.start_node.getElement().offsetTop + link_element.start_node_offsetTop) 
					link_element.line_segments[0].style.top = '50%';
				
				link_element.line_segments[1].style.top = '50%';
				
				link_element.line_segments[2].style.height =  '50%';
			}
		}
		
		if(link_element.start_node_face == 'Right') {
			//Dealing with the right edge
			//Default styles first
			link_element.line_segments[0].style.height = '1px';
			link_element.line_segments[0].style.width = '100%';
			link_element.line_segments[0].style.position = 'absolute';
			link_element.line_segments[0].style.top = '0px';
			link_element.line_segments[0].style.left = '0px';
			
			link_element.line_segments[1].style.height = '100%';
			link_element.line_segments[1].style.width = '1px';
			link_element.line_segments[1].style.position = 'absolute';
			link_element.line_segments[1].style.top = '0px';
			link_element.line_segments[1].style.left = '100%';
			
			link_element.line_segments[2].style.height = '1px';
			link_element.line_segments[2].style.width = '1px';
			link_element.line_segments[2].style.position = 'absolute';
			link_element.line_segments[2].style.top = '0px';
			link_element.line_segments[2].style.left = '100%';
			
			//Check if we are to the left of the origin (invert)
			if( link_element.getElement().offsetLeft < link_element.start_node.getElement().offsetLeft + link_element.start_node_offsetLeft )
			{
				link_element.line_segments[1].style.left = '0px';
			} else 
			{
				link_element.line_segments[1].style.left = '100%';
			}
			
			//Check if we are below the origin (invert)
			if( link_element.getElement().offsetTop < link_element.start_node.getElement().offsetTop + link_element.start_node_offsetTop )
			{
				link_element.line_segments[0].style.top = '100%';
			} else 
			{
				link_element.line_segments[0].style.top = '0px';
				link_element.line_segments[2].style.top = '100%';
			}
			
			//Now target those snaps bro
			if( target_element && (target_element.end_node_offsetLeft <= 0 || target_element.end_node_offsetLeft >= target_element.getElement().clientWidth) )
			{
				if( link_element.getElement().offsetLeft < link_element.start_node.getElement().offsetLeft + link_element.start_node_offsetLeft ) {
					link_element.line_segments[0].style.left = '50%';
					link_element.line_segments[2].style.left = '0px';
				} else {
					link_element.line_segments[0].style.left = '0px';
					link_element.line_segments[2].style.left = '50%';
				}
				
				link_element.line_segments[0].style.width = '50%';
				
				link_element.line_segments[1].style.left = '50%';
				
				link_element.line_segments[2].style.width = '50%';
			}
		}
		
		if(link_element.start_node_face == 'Top') {
			//Dealing with top edge here, be careful
			//Default styles first
			link_element.line_segments[0].style.height = '100%';
			link_element.line_segments[0].style.width = '1px';
			link_element.line_segments[0].style.position = 'absolute';
			link_element.line_segments[0].style.top = '0px';
			link_element.line_segments[0].style.left = '0px';
			
			link_element.line_segments[1].style.height = '1px';
			link_element.line_segments[1].style.width = '100%';
			link_element.line_segments[1].style.position = 'absolute';
			link_element.line_segments[1].style.top = '0px';
			link_element.line_segments[1].style.left = '0px';
			
			link_element.line_segments[2].style.height = '1px';
			link_element.line_segments[2].style.width = '1px';
			link_element.line_segments[2].style.position = 'absolute';
			link_element.line_segments[2].style.top = '0px';
			link_element.line_segments[2].style.left = '100%';
			
			//Check if we are to the left of the origin (invert)
			if( link_element.getElement().offsetLeft < link_element.start_node.getElement().offsetLeft + link_element.start_node_offsetLeft )
			{
				link_element.line_segments[0].style.left = '100%';
				link_element.line_segments[2].style.left = '0px';
			} else 
			{
				link_element.line_segments[0].style.left = '0px';
				link_element.line_segments[2].style.left = '100%';
			}
			
			//Check if we are below the origin (invert)
			if( link_element.getElement().offsetTop < link_element.start_node.getElement().offsetTop + link_element.start_node_offsetTop )
			{
				link_element.line_segments[1].style.top = '0px';
				link_element.line_segments[2].style.top = '0px';
			} else 
			{
				link_element.line_segments[1].style.top = '100%';
				link_element.line_segments[2].style.top = '50%';
			}
			
			//Take care of targeting snaps
			if( target_element && (target_element.end_node_offsetTop <= 0 || target_element.end_node_offsetTop >= target_element.getElement().clientHeight) )
			{
				if( link_element.getElement().offsetTop < link_element.start_node.getElement().offsetTop + link_element.start_node_offsetTop )
					link_element.line_segments[0].style.top = '50%';
					
				link_element.line_segments[0].style.height = '50%';
				
				link_element.line_segments[1].style.top = '50%';
				
				link_element.line_segments[2].style.height = '50%';
			}
			
		}
		
		if(link_element.start_node_face == 'Left') {
			//Dealing with the left edge
			//Default styles first
			link_element.line_segments[0].style.height = '1px';
			link_element.line_segments[0].style.width = '100%';
			link_element.line_segments[0].style.position = 'absolute';
			link_element.line_segments[0].style.top = '0px';
			link_element.line_segments[0].style.left = '0px';
			
			link_element.line_segments[1].style.height = '100%';
			link_element.line_segments[1].style.width = '1px';
			link_element.line_segments[1].style.position = 'absolute';
			link_element.line_segments[1].style.top = '0px';
			link_element.line_segments[1].style.left = '0px';
			
			link_element.line_segments[2].style.height = '1px';
			link_element.line_segments[2].style.width = '1px';
			link_element.line_segments[2].style.position = 'absolute';
			link_element.line_segments[2].style.top = '0px';
			link_element.line_segments[2].style.left = '0px';
			
			//Check if we are to the left of the origin (invert)
			if( link_element.getElement().offsetLeft < link_element.start_node.getElement().offsetLeft + link_element.start_node_offsetLeft )
			{
				link_element.line_segments[1].style.left = '0px';
			} else 
			{
				link_element.line_segments[1].style.left = '100%';
			}
			
			//Check if we are below the origin (invert)
			if( link_element.getElement().offsetTop < link_element.start_node.getElement().offsetTop + link_element.start_node_offsetTop )
			{
				link_element.line_segments[0].style.top = '100%';
			} else 
			{
				link_element.line_segments[0].style.top = '0px';
				link_element.line_segments[2].style.top = '100%';
			}
			
			//Take care of targeting snaps
			if( target_element && (target_element.end_node_offsetLeft <= 0 || target_element.end_node_offsetLeft >= target_element.getElement().clientWidth) )
			{
				link_element.line_segments[0].style.width = '50%';
				if( link_element.getElement().offsetLeft < link_element.start_node.getElement().offsetLeft + link_element.start_node_offsetLeft ) {
					link_element.line_segments[0].style.left = '50%';
					link_element.line_segments[2].style.left = '0px';
				} else {
					link_element.line_segments[0].style.left = '0px';
					link_element.line_segments[2].style.left = '50%';
				}
					
				link_element.line_segments[1].style.left = '50%';
				
				link_element.line_segments[2].style.width = '50%';
			}
		}
	};
}