maxGraph/javascript/examples/dbeditor.html

1032 lines
27 KiB
HTML
Executable File

<!--
$Id: dbeditor.html,v 1.25 2012-01-09 06:35:02 gaudenz Exp $
Copyright (c) 2006-2010, JGraph Ltd
dbEditor example for mxGraph. Uses Google Gears database for database
content editor. The database contains Persons, Locations and connections
between persons and locations. The database is updated as the diagram
is being changed using the callbacks provided by the model.
-->
<html>
<head>
<title>dbEditor Example</title>
<script type="text/javascript">
mxBasePath = '../src';
</script>
<script type="text/javascript" src="../src/js/mxClient.js"></script>
<script type="text/javascript" src="http://code.google.com/apis/gears/gears_init.js"></script>
<script type="text/javascript">
// Program starts here. The document.onLoad executes the
// mxApplication constructor with a given configuration.
// In the config file, the mxEditor.onInit method is
// overridden to invoke this global function as the
// last step in the editor constructor.
function main(graphContainer, toolbarContainer, sidebarContainer)
{
// Checks if the browser is supported
if (!mxClient.isBrowserSupported())
{
// Displays an error message if the browser is not supported
mxUtils.error('Browser is not supported!', 200, false);
}
else
{
// Checks if Google Gears is installed and redirects
if (!window.google || !google.gears)
{
location.href = 'http://gears.google.com/?action=install';
}
else
{
// Creates a wrapper editor with a graph inside the given container
var editor = new mxEditor();
editor.setGraphContainer(graphContainer);
var graph = editor.graph;
var model = graph.model;
var parent = graph.getDefaultParent();
// Allows new connections to be created
graph.setConnectable(true);
// Uses a different perimeter style
var style = graph.getStylesheet().getDefaultVertexStyle();
style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter;
style[mxConstants.STYLE_SPACING] = 8;
createSidebar(sidebarContainer, editor);
createToolbar(toolbarContainer, editor);
// Returns a label for the cell
graph.getLabel = function(cell)
{
if (cell.value != null)
{
if (cell.value instanceof Person)
{
return cell.value.lastName + ', '+ cell.value.firstName;
}
else if (cell.value instanceof Location)
{
return cell.value.building + ' ' + cell.value.office;
}
else if (cell.value instanceof LocatedAt)
{
return cell.value.remark;
}
}
return mxGraph.prototype.getLabel.apply(this, arguments); // "supercall"
};
// Returns the type as the tooltip for column cells
graph.getTooltip = function(cell)
{
return cell.getId();
};
// Disallows drag and drop into edges
graph.isSplitDropTarget = function()
{
return false;
};
// Only allow connections from person to location
graph.getEdgeValidationError = function(edge, source, target)
{
if (source != null && source.value instanceof Person &&
target != null && target.value instanceof Location)
{
// Allows connections from person to location
return null;
}
else if (source == target || target == null)
{
// Loops and connections to nothing are not allowed
// but no error message is displayed in these cases
// for easier double clicks
return '';
}
return 'Can only connect person to location';
}
// Shows property window for cell editing
graph.startEditingAtCell = function(cell)
{
showProperties(this, cell);
}
// Creates user object for new edges
graph.connectionHandler.factoryMethod = function(source, target)
{
if (source.value instanceof Person &&
target.value instanceof Location)
{
var locatedAt = new LocatedAt();
locatedAt.remark = 'works in';
edge = new mxCell(locatedAt);
edge.setEdge(true);
return edge;
}
}
// Create a model for the database
model.db = google.gears.factory.create('beta.database', '1.0');
var dbname = 'test'; //mxUtils.prompt('Please enter database name', 'test');
// Shows the database name in the window
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.top = 0;
div.style.right = 0;
div.style.padding = 4;
mxUtils.write(div, 'DB: '+dbname);
document.body.appendChild(div);
model.db.open(dbname);
model.transactionInProgress = false;
// Global function to execute SQL
model.exec = function(sql, args)
{
mxLog.show();
if (!this.transactionInProgress)
{
this.transactionInProgress = true;
this.db.execute('BEGIN TRANSACTION');
mxLog.debug('BEGIN TRANSACTION');
}
mxLog.debug(sql);
return this.db.execute(sql, args);
}
// FIXME: Cannot override mxEventSource.fireEvent
model.addListener(mxEvent.CHANGE, function()
{
if (model.transactionInProgress)
{
model.db.execute('COMMIT TRANSACTION');
mxLog.debug('COMMIT TRANSACTION');
this.transactionInProgress = false;
}
});
// Create the tables
model.db.execute('CREATE TABLE IF NOT EXISTS PERSON (PERSON_ID INTEGER PRIMARY KEY AUTOINCREMENT, FIRST_NAME TEXT, LAST_NAME TEXT)');
model.db.execute('CREATE TABLE IF NOT EXISTS LOCATION (LOCATION_ID INTEGER PRIMARY KEY AUTOINCREMENT, OFFICE TEXT, BUILDING TEXT)');
model.db.execute('CREATE TABLE IF NOT EXISTS LOCATED_AT (LOCATED_AT_ID INTEGER PRIMARY KEY AUTOINCREMENT, PERSON_ID INTEGER, LOCATION_ID INTEGER, REMARK TEXT)');
// Returns some specific style based on user object
model.getStyle = function(cell)
{
if (cell.value != null)
{
if (cell.value instanceof Person)
{
return 'shape=label;image=editors/images/overlays/user3.png;imageWidth=16;imageHeight=16;spacing=6;spacingLeft=16;fillColor=white;shadow=1';
}
else if (cell.value instanceof Location)
{
return 'shape=label;image=editors/images/overlays/house.png;imageWidth=16;imageHeight=16;spacing=6;spacingLeft=16;fillColor=white;shadow=1;rounded=1';
}
else if (cell.value instanceof LocatedAt)
{
return 'strokeColor=black;strokeWidth=1';
}
}
return mxGraphModel.prototype.getStyle.apply(this, arguments); // "supercall"
}
model.cellAdded = function(cell)
{
try
{
if (cell.value != null &&
cell.value.id == null)
{
if (cell.value instanceof Person)
{
this.exec('INSERT INTO PERSON (FIRST_NAME, LAST_NAME) VALUES (?, ?)',
[cell.value.firstName, cell.value.lastName]);
var sequence = getSequence(editor.graph.model.db, 'PERSON');
cell.value.id = sequence;
cell.setId('PERSON_'+sequence);
}
else if (cell.value instanceof Location)
{
this.exec('INSERT INTO LOCATION (OFFICE, BUILDING) VALUES (?, ?)',
[cell.value.office, cell.value.building]);
var sequence = getSequence(editor.graph.model.db, 'LOCATION');
cell.value.id = sequence;
cell.setId('LOCATION_'+sequence);
}
else if (cell.value instanceof LocatedAt)
{
this.exec('INSERT INTO LOCATED_AT (REMARK) VALUES (?)',
[cell.value.remark]);
var sequence = getSequence(editor.graph.model.db, 'LOCATED_AT');
cell.value.id = sequence;
cell.setId('LOCATEDAT_'+sequence);
}
}
}
catch (e)
{
mxUtils.alert(e.message);
}
mxGraphModel.prototype.cellAdded.apply(this, arguments); // "supercall"
}
model.cellRemoved = function(cell)
{
try
{
if (cell.value != null && cell.value.id != null)
{
if (cell.value instanceof Person)
{
this.exec('DELETE FROM PERSON WHERE PERSON_ID = ?', [cell.value.id]);
cell.value.id = null;
}
else if (cell.value instanceof Location)
{
this.exec('DELETE FROM LOCATION WHERE LOCATION_ID = ?', [cell.value.id]);
cell.value.id = null;
}
else if (cell.value instanceof LocatedAt)
{
this.exec('DELETE FROM LOCATED_AT WHERE LOCATED_AT_ID = ?', [cell.value.id]);
cell.value.id = null;
}
}
}
catch (e)
{
mxUtils.alert(e.message);
}
mxGraphModel.prototype.cellRemoved.apply(this, arguments); // "supercall"
}
model.valueForCellChanged = function(cell, value)
{
try
{
if (value != null &&
value.id != null)
{
if (value instanceof Person)
{
this.exec('UPDATE PERSON SET FIRST_NAME=?, LAST_NAME=? WHERE PERSON_ID=?',
[value.firstName, value.lastName, value.id]);
}
else if (value instanceof Location)
{
this.exec('UPDATE LOCATION SET OFFICE=?, BUILDING=? WHERE LOCATION_ID=?',
[value.office, value.building, value.id]);
}
else if (value instanceof LocatedAt)
{
this.exec('UPDATE LOCATED_AT SET REMARK=? WHERE LOCATED_AT_ID=?',
[value.remark, value.id]);
}
}
}
catch (e)
{
mxUtils.alert(e.message);
}
return mxGraphModel.prototype.valueForCellChanged.apply(this, arguments); // "supercall"
}
model.terminalForCellChanged = function(edge, terminal, isSource)
{
try
{
if (edge.value != null && edge.value instanceof LocatedAt)
{
if (isSource && terminal != null &&
terminal.value instanceof Person &&
edge.value.person != terminal.value)
{
this.exec('UPDATE LOCATED_AT SET PERSON_ID=? WHERE LOCATED_AT_ID=?',
[terminal.value.id, edge.value.id]);
edge.value.person = terminal.value;
}
else if (isSource && terminal == null)
{
this.exec('UPDATE LOCATED_AT SET PERSON_ID=NULL WHERE LOCATED_AT_ID=?',
[edge.value.id]);
edge.value.person = null;
}
else if (!isSource && terminal != null &&
terminal.value instanceof Location &&
edge.value.location != terminal.value)
{
this.exec('UPDATE LOCATED_AT SET LOCATION_ID=? WHERE LOCATED_AT_ID=?',
[terminal.value.id, edge.value.id]);
edge.value.location = terminal.value;
}
else if (!isSource && terminal == null)
{
this.exec('UPDATE LOCATED_AT SET LOCATION_ID=NULL WHERE LOCATED_AT_ID=?',
[edge.value.id]);
edge.value.location = null;
}
}
}
catch (e)
{
mxUtils.alert(e.message);
}
return mxGraphModel.prototype.terminalForCellChanged.apply(this, arguments); // "supercall"
}
}
}
};
// Creates the sidebar
function createSidebar(container, editor)
{
mxUtils.writeln(container, 'Search:');
var input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('size', '16');
container.appendChild(input);
mxUtils.br(container);
var button = document.createElement('button');
mxUtils.write(button, 'Search');
container.appendChild(button);
mxUtils.br(container);
mxUtils.br(container);
mxUtils.writeln(container, 'Results:');
var div = document.createElement('div');
div.style.cssText = 'border-style:none;border-width:1px;border-color:black;'+
'width:120px;max-height:260px;padding:2px;overflow:auto;'+
'font-size:10pt;font-family:Arial,Helvetica;';
container.appendChild(div);
mxUtils.write(div, 'No results');
var onClick = function()
{
removeChildren(div);
var results = false;
var rs = editor.graph.model.db.execute('SELECT * FROM PERSON WHERE '+
'FIRST_NAME || " " || LAST_NAME LIKE ? '+
'OR LAST_NAME || ", " || FIRST_NAME LIKE ? '+
'ORDER BY LAST_NAME || ", " || FIRST_NAME',
['%'+input.value+'%', '%'+input.value+'%']);
results = rs.isValidRow();
while (rs.isValidRow())
{
var id = rs.fieldByName('PERSON_ID');
var lastName = rs.fieldByName('LAST_NAME');
var firstName = rs.fieldByName('FIRST_NAME');
// Note: When using a DIV the DnD does only work in FF if
// it is started from over the area which contains the text
var result = document.createElement('span');
result.style.cssText = 'cursor:default;white-space:nowrap;font-size:10pt;font-family:Arial,Helvetica;';
var img = document.createElement('img');
img.setAttribute('src', 'editors/images/overlays/user3.png');
img.style.marginTop = 8;
img.style.marginRight = 4;
result.appendChild(img);
mxUtils.writeln(result, lastName+', '+firstName);
div.appendChild(result);
var funct = createPersonInsertFunction(editor, id, lastName, firstName);
mxUtils.makeDraggable(result, editor.graph, funct);
rs.next();
}
rs.close();
rs = editor.graph.model.db.execute('SELECT * FROM LOCATION WHERE '+
'BUILDING || " " || OFFICE LIKE ? '+
'ORDER BY BUILDING || " " || OFFICE',
['%'+input.value+'%']);
results = results || rs.isValidRow();
while (rs.isValidRow())
{
var id = rs.fieldByName('LOCATION_ID');
var building = rs.fieldByName('BUILDING');
var office = rs.fieldByName('OFFICE');
// Note: When using a DIV the DnD does only work in FF if
// it is started from over the area which contains the text
var result = document.createElement('span');
result.style.cssText = 'cursor:default;white-space:nowrap;font-size:10pt;font-family:Arial,Helvetica;';
var img = document.createElement('img');
img.setAttribute('src', 'editors/images/overlays/house.png');
img.style.marginTop = 8;
img.style.marginRight = 4;
result.appendChild(img);
mxUtils.writeln(result, building+' '+office);
div.appendChild(result);
var funct = createLocationInsertFunction(editor, id, office, building);
mxUtils.makeDraggable(result, editor.graph, funct);
rs.next();
}
rs.close();
if (!results)
{
mxUtils.write(div, 'No results');
}
}
mxEvent.addListener(button, 'click', onClick);
// Executes onClick if return is pressed
mxEvent.addListener(input, 'keydown', function(evt)
{
if (evt.keyCode == 13)
{
onClick();
}
});
};
function createPersonInsertFunction(editor, id, lastName, firstName)
{
return function(graph, evt)
{
var pt = editor.graph.getPointForEvent(evt);
var cellId = 'PERSON_'+id;
var cell = editor.graph.model.getCell(cellId);
if (cell == null)
{
var person = new Person(id);
person.firstName = firstName;
person.lastName = lastName;
cell = new mxCell(person, new mxGeometry(0, 0, 0, 0));
cell.setId(cellId);
cell.setConnectable(true);
cell.setVertex(true);
editor.graph.model.beginUpdate();
try
{
editor.addVertex(null, cell, pt.x, pt.y);
editor.graph.updateCellSize(cell);
var rs = editor.graph.model.db.execute(
'SELECT * FROM LOCATED_AT WHERE PERSON_ID=?', [id]);
while (rs.isValidRow())
{
var locationId = rs.fieldByName('LOCATION_ID');
var target = editor.graph.model.getCell('LOCATION_'+locationId);
if (target != null)
{
var locatedAtId = rs.fieldByName('LOCATED_AT_ID');
if (editor.graph.model.getCell('LOCATEDAT_'+locatedAtId) == null)
{
var locatedAt = new LocatedAt(locatedAtId);
locatedAt.remark = rs.fieldByName('REMARK');
locatedAt.person = cell.value;
locatedAt.location = target.value;
var edge = new mxCell(locatedAt);
edge.setId('LOCATEDAT_'+locatedAtId);
edge.setEdge(true);
edge.setGeometry(new mxGeometry());
edge.getGeometry().relative = true;
editor.graph.addEdge(edge, null, cell, target);
}
}
rs.next();
}
rs.close();
}
finally
{
editor.graph.model.endUpdate();
}
}
else
{
var geo = editor.graph.model.getGeometry(cell);
if (geo != null)
{
geo = geo.clone();
geo.x = pt.x;
geo.y = pt.y;
editor.graph.model.setGeometry(cell, geo);
}
}
editor.graph.scrollCellToVisible(cell);
editor.graph.setSelectionCell(cell);
mxEvent.consume(evt);
}
};
function createLocationInsertFunction(editor, id, office, building)
{
return function(graph, evt)
{
var pt = editor.graph.getPointForEvent(evt);
var cellId = 'LOCATION_'+id;
var cell = editor.graph.model.getCell(cellId);
if (cell == null)
{
var location = new Location(id);
location.office = office;
location.building = building;
cell = new mxCell(location, new mxGeometry(0, 0, 0, 0));
cell.setId(cellId);
cell.setConnectable(true);
cell.setVertex(true);
editor.graph.model.beginUpdate();
try
{
editor.addVertex(null, cell, pt.x, pt.y);
editor.graph.updateCellSize(cell);
var rs = editor.graph.model.db.execute(
'SELECT * FROM LOCATED_AT WHERE LOCATION_ID=?', [id]);
while (rs.isValidRow())
{
var personId = rs.fieldByName('PERSON_ID');
var source = editor.graph.model.getCell('PERSON_'+personId);
if (source != null)
{
var locatedAtId = rs.fieldByName('LOCATED_AT_ID');
if (editor.graph.model.getCell('LOCATEDAT_'+locatedAtId) == null)
{
var locatedAt = new LocatedAt(locatedAtId);
locatedAt.remark = rs.fieldByName('REMARK');
locatedAt.person = source.value;
locatedAt.location = cell.value;
var edge = new mxCell(locatedAt);
edge.setId('LOCATEDAT_'+locatedAtId);
edge.setEdge(true);
edge.setGeometry(new mxGeometry());
edge.getGeometry().relative = true;
editor.graph.addEdge(edge, null, source, cell);
}
}
rs.next();
}
rs.close();
}
finally
{
editor.graph.model.endUpdate();
}
}
else
{
var geo = editor.graph.model.getGeometry(cell);
if (geo != null)
{
geo = geo.clone();
geo.x = pt.x;
geo.y = pt.y;
editor.graph.model.setGeometry(cell, geo);
}
}
editor.graph.scrollCellToVisible(cell);
editor.graph.setSelectionCell(cell);
mxEvent.consume(evt);
}
};
// Creates the toolbar
function createToolbar(container, editor)
{
var model = editor.graph.model;
var button = null;
button = document.createElement('button');
mxUtils.write(button, 'Clear');
mxEvent.addListener(button, 'click', function(evt)
{
model.exec('DELETE FROM PERSON');
model.exec('DELETE FROM LOCATION');
model.exec('DELETE FROM LOCATED_AT');
var root = new mxCell();
root.insert(new mxCell());
model.setRoot(root);
editor.resetHistory();
mxUtils.alert('Database has been cleared');
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Dump');
mxEvent.addListener(button, 'click', function(evt)
{
var s = 'Person table:\n';
s += dump(model.db.execute('SELECT * FROM PERSON'));
s += '\nLocation table:\n';
s += dump(model.db.execute('SELECT * FROM LOCATION'));
s += '\nLocated_at table:\n';
s += dump(model.db.execute('SELECT * FROM LOCATED_AT'));
mxUtils.popup(s);
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Create Person');
mxEvent.addListener(button, 'click', function(evt)
{
var person = new Person();
person.firstName = 'First';
person.lastName = 'Name';
var cell = new mxCell(person, new mxGeometry(0, 0, 0, 0));
cell.setConnectable(true);
cell.setVertex(true);
model.beginUpdate();
try
{
editor.addVertex(null, cell, 10, 10);
editor.graph.updateCellSize(cell);
showProperties(editor.graph, cell);
}
finally
{
model.endUpdate();
}
editor.graph.setSelectionCell(cell);
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Create Location');
mxEvent.addListener(button, 'click', function(evt)
{
var location = new Location();
location.office = 'Office';
location.building = 'Bldg';
var cell = new mxCell(location, new mxGeometry(0, 0, 0, 0));
cell.setConnectable(true);
cell.setVertex(true);
model.beginUpdate();
try
{
editor.addVertex(null, cell, 10, 10);
editor.graph.updateCellSize(cell);
showProperties(editor.graph, cell);
}
finally
{
model.endUpdate();
}
editor.graph.setSelectionCell(cell);
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Delete');
mxEvent.addListener(button, 'click', function(evt)
{
editor.graph.removeCells();
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Undo');
mxEvent.addListener(button, 'click', function(evt)
{
editor.execute('undo');
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Redo');
mxEvent.addListener(button, 'click', function(evt)
{
editor.execute('redo');
});
container.appendChild(button);
button = document.createElement('button');
mxUtils.write(button, 'Arrange');
mxEvent.addListener(button, 'click', function(evt)
{
// Creates a layout algorithm to be used
// with the graph
var layout = new mxFastOrganicLayout(editor.graph);
// Moves stuff wider apart than usual
layout.forceConstant = 120;
layout.execute(editor.graph.getDefaultParent());
});
container.appendChild(button);
};
// Dumps the given table to the console
function dump(rs)
{
var result = [];
var fieldCount = rs.fieldCount();
while (rs.isValidRow())
{
result.push('{');
for (var i=0; i<fieldCount; i++)
{
result.push(' '+rs.fieldName(i)+': '+rs.field(i));
}
result.push('}');
rs.next();
}
rs.close();
return result.join('\n');
};
// Removes all child nodes from the given container
function removeChildren(container)
{
while (container.firstChild != null)
{
container.removeChild(container.firstChild);
}
};
// Gets the current sequence number for the given table
function getSequence(db, tablename)
{
var rs = db.execute('SELECT seq FROM sqlite_sequence WHERE name=?', [tablename])
var seq = null;
if (rs.isValidRow())
{
seq = parseInt(rs.fieldByName('seq'));
}
rs.close();
return seq;
};
function showProperties(graph, cell)
{
// Creates a form for the user object inside
// the cell
var form = new mxForm('properties');
// Adds a field for the columnname
var fields = [];
var firstField = null;
for (var i in cell.value)
{
if (i != 'id' &&
typeof(cell.value[i]) != 'function' &&
typeof(cell.value[i]) != 'object')
{
fields[i] = form.addText(i, cell.value[i]);
if (firstField == null)
{
firstField = fields[i];
}
}
}
var wnd = null;
// Defines the function to be executed when the
// OK button is pressed in the dialog
var okFunction = function()
{
var clone = cell.value.clone();
for (var i in fields)
{
clone[i] = fields[i].value;
}
graph.model.beginUpdate();
try
{
graph.model.setValue(cell, clone);
graph.updateCellSize(cell);
}
finally
{
graph.model.endUpdate();
}
wnd.destroy();
}
// Defines the function to be executed when the
// Cancel button is pressed in the dialog
var cancelFunction = function()
{
wnd.destroy();
}
form.addButtons(okFunction, cancelFunction);
wnd = showModalWindow(cell.getId(), form.table, 240, 240);
firstField.focus();
firstField.select();
};
function showModalWindow(title, content, width, height)
{
var background = document.createElement('div');
background.style.position = 'absolute';
background.style.left = '0px';
background.style.top = '0px';
background.style.right = '0px';
background.style.bottom = '0px';
background.style.background = 'black';
mxUtils.setOpacity(background, 50);
document.body.appendChild(background);
if (mxClient.IS_IE)
{
new mxDivResizer(background);
}
var x = Math.max(0, document.body.scrollWidth/2-width/2);
var y = Math.max(10, (document.body.scrollHeight ||
document.documentElement.scrollHeight)/2-height*2/3);
var wnd = new mxWindow(title, content, x, y, width, height, false, true);
wnd.setClosable(true);
// Fades the background out after after the window has been closed
wnd.addListener(mxEvent.DESTROY, function(sender, evt)
{
mxEffects.fadeOut(background, 50, true,
10, 30, true);
});
wnd.setVisible(true);
return wnd;
};
// Person
function Person(id)
{
this.id = id;
};
Person.prototype.clone = function()
{
return mxUtils.clone(this);
};
// Location
function Location(id)
{
this.id = id;
}
Location.prototype.clone = function()
{
return mxUtils.clone(this);
};
// LocatedAt
function LocatedAt(id)
{
this.id = id;
};
LocatedAt.prototype.clone = function()
{
return mxUtils.clone(this, ["person", "terminal"]);
};
</script>
</head>
<body onload="main(document.getElementById('graph'),
document.getElementById('toolbar'),
document.getElementById('sidebar'));">
<table border="0" width="100%" height="100%">
<tr>
<td id="toolbar" valign="top" colspan="2" style="height:10px;">
<!-- Toolbar Here -->
</td>
</tr>
<tr>
<td id="sidebar" style="width:10px;" valign="top">
<!-- Sidebar Here -->
</td>
<td style="border-width:1px;border-style:solid;border-color:black;width:100%;">
<div id="graph" style="width:100%;height:100%;cursor:default;overflow:hidden;">
<!-- Graph Here -->
</div>
</td>
</tr>
</table>
</body>
</html>