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%';
}
}
};
}