add most active org link
add most active org link

file:b/.gitignore (new)
  *.py[cod]
 
  # C extensions
  *.so
 
  # Packages
  *.egg
  *.egg-info
  dist
  build
  eggs
  parts
  bin
  var
  sdist
  develop-eggs
  .installed.cfg
  lib
  lib64
 
  # Installer logs
  pip-log.txt
 
  # Unit test / coverage reports
  .coverage
  .tox
  nosetests.xml
 
  # Translations
  *.mo
 
  # Mr Developer
  .mr.developer.cfg
  .project
  .pydevproject
 
file:b/README.md (new)
  # ckanext-dga-stats
 
  Fork of CKAN's built-in Statistics plugin modified for data.gov.au
 
  * Remove private datasets from all statistics (except top users)
  * Add summary page
  * Add activity summary page
  * Add organisation public/private dataset count page
 
  # this is a namespace package
  try:
  import pkg_resources
  pkg_resources.declare_namespace(__name__)
  except ImportError:
  import pkgutil
  __path__ = pkgutil.extend_path(__path__, __name__)
 
# empty file needed for pylons to find templates in this directory  
 
 Binary files a/ckanext/dga-stats/__init__.pyc and /dev/null differ
import ckan.plugins as p  
from ckan.lib.base import BaseController, config  
import stats as stats_lib  
import ckan.lib.helpers as h  
 
class StatsController(BaseController):  
 
def index(self):  
c = p.toolkit.c  
stats = stats_lib.Stats()  
rev_stats = stats_lib.RevisionStats()  
c.top_rated_packages = stats.top_rated_packages()  
c.most_edited_packages = stats.most_edited_packages()  
c.largest_groups = stats.largest_groups()  
c.top_tags = stats.top_tags()  
c.top_package_owners = stats.top_package_owners()  
c.new_packages_by_week = rev_stats.get_by_week('new_packages')  
c.deleted_packages_by_week = rev_stats.get_by_week('deleted_packages')  
c.num_packages_by_week = rev_stats.get_num_packages_by_week()  
c.package_revisions_by_week = rev_stats.get_by_week('package_revisions')  
 
# Used in the legacy CKAN templates.  
c.packages_by_week = []  
 
# Used in new CKAN templates gives more control to the templates for formatting.  
c.raw_packages_by_week = []  
for week_date, num_packages, cumulative_num_packages in c.num_packages_by_week:  
c.packages_by_week.append('[new Date(%s), %s]' % (week_date.replace('-', ','), cumulative_num_packages))  
c.raw_packages_by_week.append({'date': h.date_str_to_datetime(week_date), 'total_packages': cumulative_num_packages})  
 
c.all_package_revisions = []  
c.raw_all_package_revisions = []  
for week_date, revs, num_revisions, cumulative_num_revisions in c.package_revisions_by_week:  
c.all_package_revisions.append('[new Date(%s), %s]' % (week_date.replace('-', ','), num_revisions))  
c.raw_all_package_revisions.append({'date': h.date_str_to_datetime(week_date), 'total_revisions': num_revisions})  
 
c.new_datasets = []  
c.raw_new_datasets = []  
for week_date, pkgs, num_packages, cumulative_num_packages in c.new_packages_by_week:  
c.new_datasets.append('[new Date(%s), %s]' % (week_date.replace('-', ','), num_packages))  
c.raw_new_datasets.append({'date': h.date_str_to_datetime(week_date), 'new_packages': num_packages})  
 
return p.toolkit.render('ckanext/stats/index.html')  
 
def leaderboard(self, id=None):  
c = p.toolkit.c  
c.solr_core_url = config.get('ckanext.stats.solr_core_url',  
'http://solr.okfn.org/solr/ckan')  
return p.toolkit.render('ckanext/stats/leaderboard.html')  
 
 
from logging import getLogger  
 
import ckan.plugins as p  
 
log = getLogger(__name__)  
 
class StatsPlugin(p.SingletonPlugin):  
'''Stats plugin.'''  
 
p.implements(p.IRoutes, inherit=True)  
p.implements(p.IConfigurer, inherit=True)  
 
def after_map(self, map):  
map.connect('stats', '/stats',  
controller='ckanext.stats.controller:StatsController',  
action='index')  
map.connect('stats_action', '/stats/{action}',  
controller='ckanext.stats.controller:StatsController')  
return map  
 
def update_config(self, config):  
templates = 'templates'  
if p.toolkit.asbool(config.get('ckan.legacy_templates', False)):  
templates = 'templates_legacy'  
p.toolkit.add_template_directory(config, templates)  
p.toolkit.add_public_directory(config, 'public')  
p.toolkit.add_resource('public/ckanext/stats', 'ckanext_stats')  
 
 Binary files a/ckanext/dga-stats/plugin.pyc and /dev/null differ
**.min.js  
**.min.css  
 
# this is a namespace package  
try:  
import pkg_resources  
pkg_resources.declare_namespace(__name__)  
except ImportError:  
import pkgutil  
__path__ = pkgutil.extend_path(__path__, __name__)  
 
# this is a namespace package  
try:  
import pkg_resources  
pkg_resources.declare_namespace(__name__)  
except ImportError:  
import pkgutil  
__path__ = pkgutil.extend_path(__path__, __name__)  
 
# this is a namespace package  
try:  
import pkg_resources  
pkg_resources.declare_namespace(__name__)  
except ImportError:  
import pkgutil  
__path__ = pkgutil.extend_path(__path__, __name__)  
 
jQuery(document).ready(function($) {  
$('form').submit(function(e) {  
e.preventDefault();  
attribute = $('#form-attribute').val();  
loadSolr(attribute);  
})  
// default! (also in html)  
loadSolr('tags');  
 
function loadSolr(attribute) {  
var url = solrCoreUrl + '/select?indent=on&wt=json&facet=true&rows=0&indent=true&facet.mincount=1&facet.limit=30&q=*:*&facet.field=' + attribute;  
function handleSolr(data) {  
var results = [];  
ourdata = data.facet_counts.facet_fields[attribute];  
var newrow = {};  
for (ii in ourdata) {  
if (ii % 2 == 0) {  
newrow.name = ourdata[ii];  
if (!newrow.name) {  
newrow.name = '[Not Specified]';  
}  
} else {  
newrow.count = ourdata[ii];  
results.push(newrow);  
newrow = {};  
}  
}  
display(results);  
}  
 
$.ajax({  
url: url,  
success: handleSolr,  
dataType: 'jsonp',  
jsonp: 'json.wrf'  
});  
}  
 
function display(results) {  
var list = $('#category-counts');  
list.html('');  
if (results.length == 0) {  
return  
}  
var maximum = results[0]['count'];  
for(ii in results) {  
maximum = Math.max(maximum, results[ii]['count']);  
}  
 
$.each(results, function(idx, row) {  
var newentry = $('<li></li>');  
newentry.append($('<a href="#">' + row['name'] + '</a>'));  
newentry.append($('<span class="count">' + row['count'] + '</a>'));  
var percent = 100 * row['count'] / maximum;  
newentry.append($('<span class="index" style="width: ' + percent + '%"></span>'));  
list.append(newentry);  
});  
}  
});  
 
.tab-content h2 {  
margin-bottom: 12px;  
}  
 
.js .tab-content {  
padding-top: 20px;  
padding-bottom: 20px;  
margin-top: 0;  
}  
 
.module-plot-canvas {  
display: block;  
width: 650px;  
height: 300px;  
margin: 20px 0;  
}  
 
<html>  
<head>  
<script type="text/javascript">  
var solrCoreUrl = 'http://solr.okfn.org/solr/ckan';  
</script>  
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>  
<script type="text/javascript" src="app.js"></script>  
 
<link type="text/css" rel="stylesheet" media="all" href="style.css" />  
</head>  
<body>  
<h1>CKAN Dataset Leaderboard</h1>  
<p>Choose a dataset attribute and find out which categories in that area have the most datasets. E.g. tags, groups, license, res_format, country.</p>  
<form>  
<label for="category">Choose area</label>  
<input type="text" value="tags" name="attribute" id="form-attribute" />  
<input type="submit" value="Dataset Counts &raquo;" name="submit" />  
</form>  
 
<div class="category-counts">  
<ul class="chartlist" id="category-counts">  
</ul>  
</div>  
</body>  
</html>  
 
/* A quick module for generating flot charts from an HTML table. Options can  
* be passed directly to flot using the data-module-* attributes. The tables  
* are currently expected to be marked up as follows:  
*  
* <table data-module="plot">  
* <thead>  
* <tr>  
* <th>X Axis</th>  
* <th>Series A Legend</th>  
* <th>Series B Legend</th>  
* </tr>  
* </thead>  
* <tbody>  
* <tr>  
* <th>X Value</th>  
* <td>Series A Y Value</td>  
* <td>Series B Y Value</td>  
* </tr>  
* ...  
* </tbody>  
* </table>  
*  
* Points are pulled out of the th/td elements using innerHTML or by looking  
* for a data-value attribute. This is useful when a more readable value  
* needs to be used in the elements contents (eg. dates). A data-type attribute  
* can also be applied to parse the value. Only data-type="date" is currently  
* supported and expects data-value to be a unix timestamp.  
*/  
this.ckan.module('plot', function (jQuery, _) {  
return {  
/* Holds the jQuery.plot() object when created */  
graph: null,  
 
/* Holds the canvas container when created */  
canvas: null,  
 
/* Default options */  
options: {  
xaxis: {},  
yaxis: {},  
legend: {position: 'nw'},  
colors: ['#ffcc33', '#ff8844']  
},  
 
/* Sets up the canvas element and parses the table.  
*  
* Returns nothing.  
*/  
initialize: function () {  
jQuery.proxyAll(this, /_on/);  
 
if (!this.el.is('table')) {  
throw new Error('CKAN module plot can only be called on table elements');  
}  
 
this.setupCanvas();  
 
// Because the canvas doesn't render correctly unless visible we must  
// listen for events that reveal the canvas and then try and re-render.  
// Currently the most common of these is the "shown" event triggered by  
// the tabs plugin.  
this.sandbox.body.on("shown", this._onShown);  
this.data = this.parseTable(this.el);  
 
this.draw();  
},  
 
/* Removes event handlers when the module is removed from the DOM.  
*  
* Returns nothing.  
*/  
teardown: function () {  
this.sandbox.body.off("shown", this._onShown);  
},  
 
/* Creates the canvas wrapper and removes the table from the document.  
*  
* Returns nothing.  
*/  
setupCanvas: function () {  
this.canvas = jQuery('<div class="module-plot-canvas">');  
this.el.replaceWith(this.canvas);  
},  
 
/* Attempts to draw the chart if the canvas is visible. If not visible the  
* graph does not render correctly. So we keep trying until it is.  
*  
* Examples  
*  
* module.draw();  
*  
* Returns nothing.  
*/  
draw: function () {  
if (!this.drawn && this.canvas.is(':visible')) {  
this.graph = jQuery.plot(this.canvas, this.data, this.options);  
}  
},  
 
/* Parses an HTML table element to build the data array for the chart.  
* The thead element provides the axis and labels for the series. The  
* first column in the tbody is used for the x-axis and subsequent  
* columns are the series.  
*  
* table - A table element to parse.  
*  
* Examples  
*  
* module.parseTable(module.el);  
*  
* Returns data object suitable for use in jQuery.plot().  
*/  
parseTable: function (table) {  
var data = [];  
var _this = this;  
 
var headings = table.find('thead tr:first th').map(function () {  
return this.innerHTML;  
});  
 
table.find('tbody tr').each(function (row) {  
var element = jQuery(this);  
var x = [];  
 
x[row] = _this.getValue(element.find('th'));  
 
element.find('td').each(function (series) {  
var value = _this.getValue(this);  
var label = headings[series + 1];  
 
data[series] = data[series] || {data: [], label: label};  
data[series].data[row] = [x[row], value];  
});  
});  
 
return data;  
},  
 
/* Retrieves the value from a td/th element. This first looks for a  
* data-value attribute on the element otherwise uses the element  
* text contents.  
*  
* A data-type attribute can also be provided to tell the module how  
* to deal with the element. By default we let jQuery.data() handle  
* the parsing but this can provide additional data. See .parseValue()  
* for more info.  
*  
* cell - An element to extract a value from.  
*  
* Examples  
*  
* var element = jQuery('<td data-value="10">Ten</td>');  
* module.getValue(element); //=> 10  
*  
* var element = jQuery('<td>20</td>');  
* module.getValue(element); //=> 20  
*  
* var element = jQuery('<td data-type="date">1343747094</td>');  
* module.getValue(element); //=> <Date Tue Jul 31 2012 16:04:54 GMT+0100 (BST)>  
*  
* Returns the parsed value.  
*/  
getValue: function (cell) {  
var item = cell instanceof jQuery ? cell : jQuery(cell);  
var type = item.data('type') || 'string';  
var value = item.data('value') || item.text();  
return this.parseValue(value, type);  
},  
 
/* Provides the ability to further format a value.  
*  
* If date is provided as a type then it expects value to be a unix  
* timestamp in seconds.  
*  
* value - The value extracted from the element.  
* type - A type string, currently only supports "date".  
*  
* Examples  
*  
* module.parseValue(10); // => 10  
* module.parseValue("cat"); // => "cat"  
* module.parseValue(1343747094, 'date'); // => <Date Tue Jul 31 2012 16:04:54 GMT+0100 (BST)>  
*  
* Returns the parsed value.  
*/  
parseValue: function (value, type) {  
if (type === 'date') {  
value = new Date(parseInt(value, 10) * 1000);  
if (!value) {  
value = 0;  
}  
}  
return value;  
},  
 
/* Event handler for when tabs are toggled. Determines if the canvas  
* resides in the shown element and attempts to re-render.  
*  
* event - The shown event object.  
*  
* Returns nothing.  
*/  
_onShown: function (event) {  
if (!this.drawn && jQuery.contains(jQuery(event.target.hash)[0], this.canvas[0])) {  
this.draw();  
}  
}  
};  
});  
 
/* Quick module to enhance the Bootstrap tags plug-in to update the url  
* hash when a tab changes to allow the user to bookmark the page.  
*  
* Each tab id must use a prefix which which will be stripped from the hash.  
* This is to prevent the page jumping when the hash fragment changes.  
*  
* prefix - The prefix used on the ids.  
*  
*/  
this.ckan.module('stats-nav', {  
/* An options object */  
options: {  
prefix: 'stats-'  
},  
 
/* Initializes the module and sets up event listeners.  
*  
* Returns nothing.  
*/  
initialize: function () {  
var location = this.sandbox.location;  
var prefix = this.options.prefix;  
var hash = location.hash.slice(1);  
var selected = this.$('[href^=#' + prefix + hash + ']');  
 
// Update the hash fragment when the tab changes.  
this.el.on('shown', function (event) {  
location.hash = event.target.hash.slice(prefix.length + 1);  
});  
 
// Show the current tab if the location provides one.  
if (selected.length) {  
selected.tab('show');  
}  
}  
});  
 
[IE conditional]  
 
lte IE 8 = vendor/excanvas.js  
 
[groups]  
 
stats =  
css/stats.css  
vendor/excanvas.js  
vendor/jquery.flot.js  
javascript/modules/plot.js  
javascript/modules/stats-nav.js  
 
div.category-counts {  
}  
 
div.category-counts-over-time {  
clear: both;  
}  
 
/***************************  
* CHART LISTS  
**************************/  
 
.chartlist {  
float: left;  
border-top: 1px solid #EEE;  
width: 90%;  
padding-left: 0;  
margin-left: 0;  
}  
 
.chartlist li {  
position: relative;  
display: block;  
border-bottom: 1px solid #EEE;  
_zoom: 1;  
}  
.chartlist li a {  
display: block;  
padding: 0.4em 4.5em 0.4em 0.5em;  
position: relative;  
z-index: 2;  
}  
.chartlist .count {  
display: block;  
position: absolute;  
top: 0;  
right: 0;  
margin: 0 0.3em;  
text-align: right;  
color: #999;  
font-weight: bold;  
font-size: 0.875em;  
line-height: 2em;  
z-index: 999;  
}  
.chartlist .index {  
display: block;  
position: absolute;  
top: 0;  
left: 0;  
height: 100%;  
background: #B8E4F5;  
text-indent: -9999px;  
overflow: hidden;  
line-height: 2em;  
}  
.chartlist li:hover {  
background: #EFEFEF;  
}  
 
 
<table data-module="plot">  
<thead>  
<tr>  
<th>X Axis</th>  
<th>Series A Legend</th>  
<th>Series B Legend</th>  
</tr>  
</thead>  
<tbody>  
<tr>  
<!-- This is the x value for each series -->  
<th data-type="date" data-value="1176073200">Apr 09, 2007</th>  
<!-- This is the y value for series a -->  
<td>20</td>  
<!-- This is the y value for series b -->  
<td>7</td>  
</tr>  
<tr>  
<th data-type="date" data-value="1176678000">Apr 16, 2007</th>  
<td>12</td>  
<td>6</td>  
</tr>  
<tr>  
<th data-type="date" data-value="1177282800">Apr 23, 2007</th>  
<td>27</td>  
<td>12</td>  
</tr>  
</tbody>  
</table>  
 
 
<!DOCTYPE html>  
<html>  
<head>  
<meta charset="utf-8" />  
<title>Mocha Tests</title>  
<link rel="stylesheet" href="../../../base/test/vendor/mocha.css" />  
</head>  
<body>  
<div id="mocha"></div>  
<div id="fixture" style="position: absolute; top: -9999px; left: -9999px"></div>  
 
<!-- Test Runner -->  
<script src="../../../base/test/vendor/sinon.js"></script>  
<script src="../../../base/test/vendor/mocha.js"></script>  
<script src="../../../base/test/vendor/chai.js"></script>  
<script>  
mocha.setup('bdd');  
var assert = chai.assert;  
 
// Export sinon.assert methods onto assert.  
sinon.assert.expose(assert, {prefix: ''});  
 
var ckan = {ENV: 'testing'};  
</script>  
 
<!-- Source -->  
<script src="../../../base/vendor/jed.js"></script>  
<script src="../../../base/vendor/jquery.js"></script>  
<script src="../../../base/vendor/bootstrap/js/bootstrap-transition.js"></script>  
<script src="../../../base/vendor/bootstrap/js/bootstrap-alert.js"></script>  
<script src="../../../base/javascript/plugins/jquery.inherit.js"></script>  
<script src="../../../base/javascript/plugins/jquery.proxy-all.js"></script>  
<script src="../../../base/javascript/sandbox.js"></script>  
<script src="../../../base/javascript/module.js"></script>  
<script src="../../../base/javascript/pubsub.js"></script>  
<script src="../../../base/javascript/client.js"></script>  
<script src="../../../base/javascript/notify.js"></script>  
<script src="../../../base/javascript/i18n.js"></script>  
<script src="../../../base/javascript/main.js"></script>  
<script src="../javascript/modules/plot.js"></script>  
<script src="../javascript/modules/stats-nav.js"></script>  
 
<!-- Suite -->  
<script src="./spec/modules/plot.spec.js"></script>  
<script src="./spec/modules/stats-nav.spec.js"></script>  
 
<script>  
beforeEach(function () {  
this.fixture = jQuery('#fixture').empty();  
});  
 
afterEach(function () {  
this.fixture.empty();  
});  
 
mocha.run().globals(['ckan', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']);  
</script>  
</body>  
</html>  
 
/*globals describe before beforeEach afterEach it assert sinon ckan jQuery */  
describe('ckan.module.PlotModule()', function () {  
var PlotModule = ckan.module.registry['plot'];  
 
before(function (done) {  
var _this = this;  
 
jQuery.get('./fixtures/table.html', function (html) {  
_this.template = html;  
done();  
});  
});  
 
beforeEach(function () {  
this.el = jQuery(this.template).appendTo(this.fixture);  
this.sandbox = ckan.sandbox();  
this.sandbox.body = this.fixture;  
this.module = new PlotModule(this.el, {}, this.sandbox);  
});  
 
afterEach(function () {  
this.module.teardown();  
});  
 
describe('.initialize()', function () {  
it('should setup the canvas element', function () {  
var target = sinon.stub(this.module, 'setupCanvas', this.module.setupCanvas);  
 
this.module.initialize();  
assert.called(target);  
});  
 
it('should draw the graph', function () {  
var target = sinon.stub(this.module, 'draw');  
 
this.module.initialize();  
assert.called(target);  
});  
 
it('should listen for "shown" events on the body', function () {  
var target = sinon.stub(this.sandbox.body, 'on');  
 
this.module.initialize();  
assert.called(target);  
assert.calledWith(target, "shown", this.module._onShown);  
});  
});  
 
describe('.teardown()', function () {  
it('should remove "shown" listeners from the body', function () {  
var target = sinon.stub(this.sandbox.body, 'off');  
 
this.module.teardown();  
assert.called(target);  
assert.calledWith(target, "shown", this.module._onShown);  
});  
});  
 
describe('.setupCanvas()', function () {  
it('should create the .canvas element', function () {  
this.module.setupCanvas();  
 
assert.isDefined(this.module.canvas);  
assert.isDefined(this.module.canvas.is('div'));  
});  
 
it('should replace the .el with the .canvas', function () {  
this.module.setupCanvas();  
assert.ok(jQuery.contains(this.sandbox.body[0], this.module.canvas[0]));  
});  
});  
 
describe('.draw()', function () {  
beforeEach(function () {  
this.plot = {};  
this.module.canvas = jQuery('<div />').appendTo(this.fixture);  
jQuery.plot = sinon.stub().returns(this.plot);  
});  
 
it('should call jQuery.plot() if the canvas is visible', function () {  
this.module.draw();  
 
assert.called(jQuery.plot);  
assert.calledWith(jQuery.plot, this.module.canvas, this.module.data, this.module.options);  
});  
 
it('should assign the .graph property', function () {  
this.module.draw();  
assert.strictEqual(this.module.graph, this.plot);  
});  
 
it('should not call jQuery.plot() if the canvas is not visible', function () {  
this.module.canvas.hide();  
this.module.draw();  
 
assert.notCalled(jQuery.plot);  
});  
});  
 
describe('.parseTable(table)', function () {  
it('should parse the contents of the provided table', function () {  
var target = this.module.parseTable(this.module.el);  
 
assert.deepEqual(target, [{  
label: 'Series A Legend',  
data: [  
[new Date(1176073200000), "20"],  
[new Date(1176678000000), "12"],  
[new Date(1177282800000), "27"]  
]  
}, {  
label: 'Series B Legend',  
data: [  
[new Date(1176073200000), "7"],  
[new Date(1176678000000), "6"],  
[new Date(1177282800000), "12"]  
]  
}]);  
});  
});  
 
describe('.getValue(cell)', function () {  
it('should extract the value from a table cell');  
it('should use the data-value attribute if present');  
it('should parse the value using the data-type');  
});  
 
describe('.parseValue(value, type)', function () {  
it('should create a date object if type == "date"');  
it('should return the value if the type is not recognised');  
});  
 
describe('._onShown(event)', function () {  
it('should call .draw() if the event.target contains the canvas');  
});  
});  
 
/*globals describe before beforeEach afterEach it assert sinon ckan jQuery */  
describe('ckan.module.StatsNavModule()', function () {  
var StatsNavModule = ckan.module.registry['stats-nav'];  
 
beforeEach(function () {  
this.el = document.createElement('div');  
this.sandbox = ckan.sandbox();  
this.sandbox.body = this.fixture;  
this.sandbox.location = {  
href: '',  
hash: ''  
};  
this.module = new StatsNavModule(this.el, {}, this.sandbox);  
 
jQuery.fn.tab = sinon.stub();  
});  
 
afterEach(function () {  
this.module.teardown();  
 
delete jQuery.fn.tab;  
});  
 
describe('.initialize()', function () {  
it('should listen for shown events and update the location.hash', function () {  
var anchor = jQuery('<a />').attr('href', '#stats-test')[0];  
 
this.module.initialize();  
this.module.el.trigger({type: 'shown', target: anchor});  
 
assert.equal(this.sandbox.location.hash, 'test');  
});  
 
it('should select the tab from the location hash on init', function () {  
var anchor = jQuery('<a />').attr('href', '#stats-test').appendTo(this.el);  
 
this.sandbox.location.hash = '#test';  
this.module.initialize();  
 
assert.called(jQuery.fn.tab);  
assert.calledWith(jQuery.fn.tab, 'show');  
});  
});  
});  
 
// Copyright 2006 Google Inc.  
//  
// Licensed under the Apache License, Version 2.0 (the "License");  
// you may not use this file except in compliance with the License.  
// You may obtain a copy of the License at  
//  
// http://www.apache.org/licenses/LICENSE-2.0  
//  
// Unless required by applicable law or agreed to in writing, software  
// distributed under the License is distributed on an "AS IS" BASIS,  
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
// See the License for the specific language governing permissions and  
// limitations under the License.  
 
 
// Known Issues:  
//  
// * Patterns only support repeat.  
// * Radial gradient are not implemented. The VML version of these look very  
// different from the canvas one.  
// * Clipping paths are not implemented.  
// * Coordsize. The width and height attribute have higher priority than the  
// width and height style values which isn't correct.  
// * Painting mode isn't implemented.  
// * Canvas width/height should is using content-box by default. IE in  
// Quirks mode will draw the canvas using border-box. Either change your  
// doctype to HTML5  
// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)  
// or use Box Sizing Behavior from WebFX  
// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)  
// * Non uniform scaling does not correctly scale strokes.  
// * Filling very large shapes (above 5000 points) is buggy.  
// * Optimize. There is always room for speed improvements.  
 
// Only add this code if we do not already have a canvas implementation  
if (!document.createElement('canvas').getContext) {  
 
(function() {  
 
// alias some functions to make (compiled) code shorter  
var m = Math;  
var mr = m.round;  
var ms = m.sin;  
var mc = m.cos;  
var abs = m.abs;  
var sqrt = m.sqrt;  
 
// this is used for sub pixel precision  
var Z = 10;  
var Z2 = Z / 2;  
 
/**  
* This funtion is assigned to the <canvas> elements as element.getContext().  
* @this {HTMLElement}  
* @return {CanvasRenderingContext2D_}  
*/  
function getContext() {  
return this.context_ ||  
(this.context_ = new CanvasRenderingContext2D_(this));  
}  
 
var slice = Array.prototype.slice;  
 
/**  
* Binds a function to an object. The returned function will always use the  
* passed in {@code obj} as {@code this}.  
*  
* Example:  
*  
* g = bind(f, obj, a, b)  
* g(c, d) // will do f.call(obj, a, b, c, d)  
*  
* @param {Function} f The function to bind the object to  
* @param {Object} obj The object that should act as this when the function  
* is called  
* @param {*} var_args Rest arguments that will be used as the initial  
* arguments when the function is called  
* @return {Function} A new function that has bound this  
*/  
function bind(f, obj, var_args) {  
var a = slice.call(arguments, 2);  
return function() {  
return f.apply(obj, a.concat(slice.call(arguments)));  
};  
}  
 
function encodeHtmlAttribute(s) {  
return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');  
}  
 
function addNamespacesAndStylesheet(doc) {  
// create xmlns  
if (!doc.namespaces['g_vml_']) {  
doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',  
'#default#VML');  
 
}  
if (!doc.namespaces['g_o_']) {  
doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',  
'#default#VML');  
}  
 
// Setup default CSS. Only add one style sheet per document  
if (!doc.styleSheets['ex_canvas_']) {  
var ss = doc.createStyleSheet();  
ss.owningElement.id = 'ex_canvas_';  
ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +  
// default size is 300x150 in Gecko and Opera  
'text-align:left;width:300px;height:150px}';  
}  
}  
 
// Add namespaces and stylesheet at startup.  
addNamespacesAndStylesheet(document);  
 
var G_vmlCanvasManager_ = {  
init: function(opt_doc) {  
if (/MSIE/.test(navigator.userAgent) && !window.opera) {  
var doc = opt_doc || document;  
// Create a dummy element so that IE will allow canvas elements to be  
// recognized.  
doc.createElement('canvas');  
doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));  
}  
},  
 
init_: function(doc) {  
// find all canvas elements  
var els = doc.getElementsByTagName('canvas');  
for (var i = 0; i < els.length; i++) {  
this.initElement(els[i]);  
}  
},  
 
/**  
* Public initializes a canvas element so that it can be used as canvas  
* element from now on. This is called automatically before the page is  
* loaded but if you are creating elements using createElement you need to  
* make sure this is called on the element.  
* @param {HTMLElement} el The canvas element to initialize.  
* @return {HTMLElement} the element that was created.  
*/  
initElement: function(el) {  
if (!el.getContext) {  
el.getContext = getContext;  
 
// Add namespaces and stylesheet to document of the element.  
addNamespacesAndStylesheet(el.ownerDocument);  
 
// Remove fallback content. There is no way to hide text nodes so we  
// just remove all childNodes. We could hide all elements and remove  
// text nodes but who really cares about the fallback content.  
el.innerHTML = '';  
 
// do not use inline function because that will leak memory  
el.attachEvent('onpropertychange', onPropertyChange);  
el.attachEvent('onresize', onResize);  
 
var attrs = el.attributes;  
if (attrs.width && attrs.width.specified) {  
// TODO: use runtimeStyle and coordsize  
// el.getContext().setWidth_(attrs.width.nodeValue);  
el.style.width = attrs.width.nodeValue + 'px';  
} else {  
el.width = el.clientWidth;  
}  
if (attrs.height && attrs.height.specified) {  
// TODO: use runtimeStyle and coordsize  
// el.getContext().setHeight_(attrs.height.nodeValue);  
el.style.height = attrs.height.nodeValue + 'px';  
} else {  
el.height = el.clientHeight;  
}  
//el.getContext().setCoordsize_()  
}  
return el;  
}  
};  
 
function onPropertyChange(e) {  
var el = e.srcElement;  
 
switch (e.propertyName) {  
case 'width':  
el.getContext().clearRect();  
el.style.width = el.attributes.width.nodeValue + 'px';  
// In IE8 this does not trigger onresize.  
el.firstChild.style.width = el.clientWidth + 'px';  
break;  
case 'height':  
el.getContext().clearRect();  
el.style.height = el.attributes.height.nodeValue + 'px';  
el.firstChild.style.height = el.clientHeight + 'px';  
break;  
}  
}  
 
function onResize(e) {  
var el = e.srcElement;  
if (el.firstChild) {  
el.firstChild.style.width = el.clientWidth + 'px';  
el.firstChild.style.height = el.clientHeight + 'px';  
}  
}  
 
G_vmlCanvasManager_.init();  
 
// precompute "00" to "FF"  
var decToHex = [];  
for (var i = 0; i < 16; i++) {  
for (var j = 0; j < 16; j++) {  
decToHex[i * 16 + j] = i.toString(16) + j.toString(16);  
}  
}  
 
function createMatrixIdentity() {  
return [  
[1, 0, 0],  
[0, 1, 0],  
[0, 0, 1]  
];  
}  
 
function matrixMultiply(m1, m2) {  
var result = createMatrixIdentity();  
 
for (var x = 0; x < 3; x++) {  
for (var y = 0; y < 3; y++) {  
var sum = 0;  
 
for (var z = 0; z < 3; z++) {  
sum += m1[x][z] * m2[z][y];  
}  
 
result[x][y] = sum;  
}  
}  
return result;  
}  
 
function copyState(o1, o2) {  
o2.fillStyle = o1.fillStyle;  
o2.lineCap = o1.lineCap;  
o2.lineJoin = o1.lineJoin;  
o2.lineWidth = o1.lineWidth;  
o2.miterLimit = o1.miterLimit;  
o2.shadowBlur = o1.shadowBlur;  
o2.shadowColor = o1.shadowColor;  
o2.shadowOffsetX = o1.shadowOffsetX;  
o2.shadowOffsetY = o1.shadowOffsetY;  
o2.strokeStyle = o1.strokeStyle;  
o2.globalAlpha = o1.globalAlpha;  
o2.font = o1.font;  
o2.textAlign = o1.textAlign;  
o2.textBaseline = o1.textBaseline;  
o2.arcScaleX_ = o1.arcScaleX_;  
o2.arcScaleY_ = o1.arcScaleY_;  
o2.lineScale_ = o1.lineScale_;  
}  
 
var colorData = {  
aliceblue: '#F0F8FF',  
antiquewhite: '#FAEBD7',  
aquamarine: '#7FFFD4',  
azure: '#F0FFFF',  
beige: '#F5F5DC',  
bisque: '#FFE4C4',  
black: '#000000',  
blanchedalmond: '#FFEBCD',  
blueviolet: '#8A2BE2',  
brown: '#A52A2A',  
burlywood: '#DEB887',  
cadetblue: '#5F9EA0',  
chartreuse: '#7FFF00',  
chocolate: '#D2691E',  
coral: '#FF7F50',  
cornflowerblue: '#6495ED',  
cornsilk: '#FFF8DC',  
crimson: '#DC143C',  
cyan: '#00FFFF',  
darkblue: '#00008B',  
darkcyan: '#008B8B',  
darkgoldenrod: '#B8860B',  
darkgray: '#A9A9A9',  
darkgreen: '#006400',  
darkgrey: '#A9A9A9',  
darkkhaki: '#BDB76B',  
darkmagenta: '#8B008B',  
darkolivegreen: '#556B2F',  
darkorange: '#FF8C00',  
darkorchid: '#9932CC',  
darkred: '#8B0000',  
darksalmon: '#E9967A',  
darkseagreen: '#8FBC8F',  
darkslateblue: '#483D8B',  
darkslategray: '#2F4F4F',  
darkslategrey: '#2F4F4F',  
darkturquoise: '#00CED1',  
darkviolet: '#9400D3',  
deeppink: '#FF1493',  
deepskyblue: '#00BFFF',  
dimgray: '#696969',  
dimgrey: '#696969',  
dodgerblue: '#1E90FF',  
firebrick: '#B22222',  
floralwhite: '#FFFAF0',  
forestgreen: '#228B22',  
gainsboro: '#DCDCDC',  
ghostwhite: '#F8F8FF',  
gold: '#FFD700',  
goldenrod: '#DAA520',  
grey: '#808080',  
greenyellow: '#ADFF2F',  
honeydew: '#F0FFF0',  
hotpink: '#FF69B4',  
indianred: '#CD5C5C',  
indigo: '#4B0082',  
ivory: '#FFFFF0',  
khaki: '#F0E68C',  
lavender: '#E6E6FA',  
lavenderblush: '#FFF0F5',  
lawngreen: '#7CFC00',  
lemonchiffon: '#FFFACD',  
lightblue: '#ADD8E6',  
lightcoral: '#F08080',  
lightcyan: '#E0FFFF',  
lightgoldenrodyellow: '#FAFAD2',  
lightgreen: '#90EE90',  
lightgrey: '#D3D3D3',  
lightpink: '#FFB6C1',  
lightsalmon: '#FFA07A',  
lightseagreen: '#20B2AA',  
lightskyblue: '#87CEFA',  
lightslategray: '#778899',  
lightslategrey: '#778899',  
lightsteelblue: '#B0C4DE',  
lightyellow: '#FFFFE0',  
limegreen: '#32CD32',  
linen: '#FAF0E6',  
magenta: '#FF00FF',  
mediumaquamarine: '#66CDAA',  
mediumblue: '#0000CD',  
mediumorchid: '#BA55D3',  
mediumpurple: '#9370DB',  
mediumseagreen: '#3CB371',  
mediumslateblue: '#7B68EE',  
mediumspringgreen: '#00FA9A',  
mediumturquoise: '#48D1CC',  
mediumvioletred: '#C71585',  
midnightblue: '#191970',  
mintcream: '#F5FFFA',  
mistyrose: '#FFE4E1',  
moccasin: '#FFE4B5',  
navajowhite: '#FFDEAD',  
oldlace: '#FDF5E6',  
olivedrab: '#6B8E23',  
orange: '#FFA500',  
orangered: '#FF4500',  
orchid: '#DA70D6',  
palegoldenrod: '#EEE8AA',  
palegreen: '#98FB98',  
paleturquoise: '#AFEEEE',  
palevioletred: '#DB7093',  
papayawhip: '#FFEFD5',  
peachpuff: '#FFDAB9',  
peru: '#CD853F',  
pink: '#FFC0CB',  
plum: '#DDA0DD',  
powderblue: '#B0E0E6',  
rosybrown: '#BC8F8F',  
royalblue: '#4169E1',  
saddlebrown: '#8B4513',  
salmon: '#FA8072',  
sandybrown: '#F4A460',  
seagreen: '#2E8B57',  
seashell: '#FFF5EE',  
sienna: '#A0522D',  
skyblue: '#87CEEB',  
slateblue: '#6A5ACD',  
slategray: '#708090',  
slategrey: '#708090',  
snow: '#FFFAFA',  
springgreen: '#00FF7F',  
steelblue: '#4682B4',  
tan: '#D2B48C',  
thistle: '#D8BFD8',  
tomato: '#FF6347',  
turquoise: '#40E0D0',  
violet: '#EE82EE',  
wheat: '#F5DEB3',  
whitesmoke: '#F5F5F5',  
yellowgreen: '#9ACD32'  
};  
 
 
function getRgbHslContent(styleString) {  
var start = styleString.indexOf('(', 3);  
var end = styleString.indexOf(')', start + 1);  
var parts = styleString.substring(start + 1, end).split(',');  
// add alpha if needed  
if (parts.length == 4 && styleString.substr(3, 1) == 'a') {  
alpha = Number(parts[3]);  
} else {  
parts[3] = 1;  
}  
return parts;  
}  
 
function percent(s) {  
return parseFloat(s) / 100;  
}  
 
function clamp(v, min, max) {  
return Math.min(max, Math.max(min, v));  
}  
 
function hslToRgb(parts){  
var r, g, b;  
h = parseFloat(parts[0]) / 360 % 360;  
if (h < 0)  
h++;  
s = clamp(percent(parts[1]), 0, 1);  
l = clamp(percent(parts[2]), 0, 1);  
if (s == 0) {  
r = g = b = l; // achromatic  
} else {  
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;  
var p = 2 * l - q;  
r = hueToRgb(p, q, h + 1 / 3);  
g = hueToRgb(p, q, h);  
b = hueToRgb(p, q, h - 1 / 3);  
}  
 
return '#' + decToHex[Math.floor(r * 255)] +  
decToHex[Math.floor(g * 255)] +  
decToHex[Math.floor(b * 255)];  
}  
 
function hueToRgb(m1, m2, h) {  
if (h < 0)  
h++;  
if (h > 1)  
h--;  
 
if (6 * h < 1)  
return m1 + (m2 - m1) * 6 * h;  
else if (2 * h < 1)  
return m2;  
else if (3 * h < 2)  
return m1 + (m2 - m1) * (2 / 3 - h) * 6;  
else  
return m1;  
}  
 
function processStyle(styleString) {  
var str, alpha = 1;  
 
styleString = String(styleString);  
if (styleString.charAt(0) == '#') {  
str = styleString;  
} else if (/^rgb/.test(styleString)) {  
var parts = getRgbHslContent(styleString);  
var str = '#', n;  
for (var i = 0; i < 3; i++) {  
if (parts[i].indexOf('%') != -1) {  
n = Math.floor(percent(parts[i]) * 255);  
} else {  
n = Number(parts[i]);  
}  
str += decToHex[clamp(n, 0, 255)];  
}  
alpha = parts[3];  
} else if (/^hsl/.test(styleString)) {  
var parts = getRgbHslContent(styleString);  
str = hslToRgb(parts);  
alpha = parts[3];  
} else {  
str = colorData[styleString] || styleString;  
}  
return {color: str, alpha: alpha};  
}  
 
var DEFAULT_STYLE = {  
style: 'normal',  
variant: 'normal',  
weight: 'normal',  
size: 10,  
family: 'sans-serif'  
};  
 
// Internal text style cache  
var fontStyleCache = {};  
 
function processFontStyle(styleString) {  
if (fontStyleCache[styleString]) {  
return fontStyleCache[styleString];  
}  
 
var el = document.createElement('div');  
var style = el.style;  
try {  
style.font = styleString;  
} catch (ex) {  
// Ignore failures to set to invalid font.  
}  
 
return fontStyleCache[styleString] = {  
style: style.fontStyle || DEFAULT_STYLE.style,  
variant: style.fontVariant || DEFAULT_STYLE.variant,  
weight: style.fontWeight || DEFAULT_STYLE.weight,  
size: style.fontSize || DEFAULT_STYLE.size,  
family: style.fontFamily || DEFAULT_STYLE.family  
};  
}  
 
function getComputedStyle(style, element) {  
var computedStyle = {};  
 
for (var p in style) {  
computedStyle[p] = style[p];  
}  
 
// Compute the size  
var canvasFontSize = parseFloat(element.currentStyle.fontSize),  
fontSize = parseFloat(style.size);  
 
if (typeof style.size == 'number') {  
computedStyle.size = style.size;  
} else if (style.size.indexOf('px') != -1) {  
computedStyle.size = fontSize;  
} else if (style.size.indexOf('em') != -1) {  
computedStyle.size = canvasFontSize * fontSize;  
} else if(style.size.indexOf('%') != -1) {  
computedStyle.size = (canvasFontSize / 100) * fontSize;  
} else if (style.size.indexOf('pt') != -1) {  
computedStyle.size = fontSize / .75;  
} else {  
computedStyle.size = canvasFontSize;  
}  
 
// Different scaling between normal text and VML text. This was found using  
// trial and error to get the same size as non VML text.  
computedStyle.size *= 0.981;  
 
return computedStyle;  
}  
 
function buildStyle(style) {  
return style.style + ' ' + style.variant + ' ' + style.weight + ' ' +  
style.size + 'px ' + style.family;  
}  
 
function processLineCap(lineCap) {  
switch (lineCap) {  
case 'butt':  
return 'flat';  
case 'round':  
return 'round';  
case 'square':  
default:  
return 'square';  
}  
}  
 
/**  
* This class implements CanvasRenderingContext2D interface as described by  
* the WHATWG.  
* @param {HTMLElement} surfaceElement The element that the 2D context should  
* be associated with  
*/  
function CanvasRenderingContext2D_(surfaceElement) {  
this.m_ = createMatrixIdentity();  
 
this.mStack_ = [];  
this.aStack_ = [];  
this.currentPath_ = [];  
 
// Canvas context properties  
this.strokeStyle = '#000';  
this.fillStyle = '#000';  
 
this.lineWidth = 1;  
this.lineJoin = 'miter';  
this.lineCap = 'butt';  
this.miterLimit = Z * 1;  
this.globalAlpha = 1;  
this.font = '10px sans-serif';  
this.textAlign = 'left';  
this.textBaseline = 'alphabetic';  
this.canvas = surfaceElement;  
 
var el = surfaceElement.ownerDocument.createElement('div');  
el.style.width = surfaceElement.clientWidth + 'px';  
el.style.height = surfaceElement.clientHeight + 'px';  
el.style.overflow = 'hidden';  
el.style.position = 'absolute';  
surfaceElement.appendChild(el);  
 
this.element_ = el;  
this.arcScaleX_ = 1;  
this.arcScaleY_ = 1;  
this.lineScale_ = 1;  
}  
 
var contextPrototype = CanvasRenderingContext2D_.prototype;  
contextPrototype.clearRect = function() {  
if (this.textMeasureEl_) {  
this.textMeasureEl_.removeNode(true);  
this.textMeasureEl_ = null;  
}  
this.element_.innerHTML = '';  
};  
 
contextPrototype.beginPath = function() {  
// TODO: Branch current matrix so that save/restore has no effect  
// as per safari docs.  
this.currentPath_ = [];  
};  
 
contextPrototype.moveTo = function(aX, aY) {  
var p = this.getCoords_(aX, aY);  
this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});  
this.currentX_ = p.x;  
this.currentY_ = p.y;  
};  
 
contextPrototype.lineTo = function(aX, aY) {  
var p = this.getCoords_(aX, aY);  
this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});  
 
this.currentX_ = p.x;  
this.currentY_ = p.y;  
};  
 
contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,  
aCP2x, aCP2y,  
aX, aY) {  
var p = this.getCoords_(aX, aY);  
var cp1 = this.getCoords_(aCP1x, aCP1y);  
var cp2 = this.getCoords_(aCP2x, aCP2y);  
bezierCurveTo(this, cp1, cp2, p);  
};  
 
// Helper function that takes the already fixed cordinates.  
function bezierCurveTo(self, cp1, cp2, p) {  
self.currentPath_.push({  
type: 'bezierCurveTo',  
cp1x: cp1.x,  
cp1y: cp1.y,  
cp2x: cp2.x,  
cp2y: cp2.y,  
x: p.x,  
y: p.y  
});  
self.currentX_ = p.x;  
self.currentY_ = p.y;  
}  
 
contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {  
// the following is lifted almost directly from  
// http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes  
 
var cp = this.getCoords_(aCPx, aCPy);  
var p = this.getCoords_(aX, aY);  
 
var cp1 = {  
x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),  
y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)  
};  
var cp2 = {  
x: cp1.x + (p.x - this.currentX_) / 3.0,  
y: cp1.y + (p.y - this.currentY_) / 3.0  
};  
 
bezierCurveTo(this, cp1, cp2, p);  
};  
 
contextPrototype.arc = function(aX, aY, aRadius,  
aStartAngle, aEndAngle, aClockwise) {  
aRadius *= Z;  
var arcType = aClockwise ? 'at' : 'wa';  
 
var xStart = aX + mc(aStartAngle) * aRadius - Z2;  
var yStart = aY + ms(aStartAngle) * aRadius - Z2;  
 
var xEnd = aX + mc(aEndAngle) * aRadius - Z2;  
var yEnd = aY + ms(aEndAngle) * aRadius - Z2;  
 
// IE won't render arches drawn counter clockwise if xStart == xEnd.  
if (xStart == xEnd && !aClockwise) {  
xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something  
// that can be represented in binary  
}  
 
var p = this.getCoords_(aX, aY);  
var pStart = this.getCoords_(xStart, yStart);  
var pEnd = this.getCoords_(xEnd, yEnd);  
 
this.currentPath_.push({type: arcType,  
x: p.x,  
y: p.y,  
radius: aRadius,  
xStart: pStart.x,  
yStart: pStart.y,  
xEnd: pEnd.x,  
yEnd: pEnd.y});  
 
};  
 
contextPrototype.rect = function(aX, aY, aWidth, aHeight) {  
this.moveTo(aX, aY);  
this.lineTo(aX + aWidth, aY);  
this.lineTo(aX + aWidth, aY + aHeight);  
this.lineTo(aX, aY + aHeight);  
this.closePath();  
};  
 
contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {  
var oldPath = this.currentPath_;  
this.beginPath();  
 
this.moveTo(aX, aY);  
this.lineTo(aX + aWidth, aY);  
this.lineTo(aX + aWidth, aY + aHeight);  
this.lineTo(aX, aY + aHeight);  
this.closePath();  
this.stroke();  
 
this.currentPath_ = oldPath;  
};  
 
contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {  
var oldPath = this.currentPath_;  
this.beginPath();  
 
this.moveTo(aX, aY);  
this.lineTo(aX + aWidth, aY);  
this.lineTo(aX + aWidth, aY + aHeight);  
this.lineTo(aX, aY + aHeight);  
this.closePath();  
this.fill();  
 
this.currentPath_ = oldPath;  
};  
 
contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {  
var gradient = new CanvasGradient_('gradient');  
gradient.x0_ = aX0;  
gradient.y0_ = aY0;  
gradient.x1_ = aX1;  
gradient.y1_ = aY1;  
return gradient;  
};  
 
contextPrototype.createRadialGradient = function(aX0, aY0, aR0,  
aX1, aY1, aR1) {  
var gradient = new CanvasGradient_('gradientradial');  
gradient.x0_ = aX0;  
gradient.y0_ = aY0;  
gradient.r0_ = aR0;  
gradient.x1_ = aX1;  
gradient.y1_ = aY1;  
gradient.r1_ = aR1;  
return gradient;  
};  
 
contextPrototype.drawImage = function(image, var_args) {  
var dx, dy, dw, dh, sx, sy, sw, sh;  
 
// to find the original width we overide the width and height  
var oldRuntimeWidth = image.runtimeStyle.width;  
var oldRuntimeHeight = image.runtimeStyle.height;  
image.runtimeStyle.width = 'auto';  
image.runtimeStyle.height = 'auto';  
 
// get the original size  
var w = image.width;  
var h = image.height;  
 
// and remove overides  
image.runtimeStyle.width = oldRuntimeWidth;  
image.runtimeStyle.height = oldRuntimeHeight;  
 
if (arguments.length == 3) {  
dx = arguments[1];  
dy = arguments[2];  
sx = sy = 0;  
sw = dw = w;  
sh = dh = h;  
} else if (arguments.length == 5) {  
dx = arguments[1];  
dy = arguments[2];  
dw = arguments[3];  
dh = arguments[4];  
sx = sy = 0;  
sw = w;  
sh = h;  
} else if (arguments.length == 9) {  
sx = arguments[1];  
sy = arguments[2];  
sw = arguments[3];  
sh = arguments[4];  
dx = arguments[5];  
dy = arguments[6];  
dw = arguments[7];  
dh = arguments[8];  
} else {  
throw Error('Invalid number of arguments');  
}  
 
var d = this.getCoords_(dx, dy);  
 
var w2 = sw / 2;  
var h2 = sh / 2;  
 
var vmlStr = [];  
 
var W = 10;  
var H = 10;  
 
// For some reason that I've now forgotten, using divs didn't work  
vmlStr.push(' <g_vml_:group',  
' coordsize="', Z * W, ',', Z * H, '"',  
' coordorigin="0,0"' ,  
' style="width:', W, 'px;height:', H, 'px;position:absolute;');  
 
// If filters are necessary (rotation exists), create them  
// filters are bog-slow, so only create them if abbsolutely necessary  
// The following check doesn't account for skews (which don't exist  
// in the canvas spec (yet) anyway.  
 
if (this.m_[0][0] != 1 || this.m_[0][1] ||  
this.m_[1][1] != 1 || this.m_[1][0]) {  
var filter = [];  
 
// Note the 12/21 reversal  
filter.push('M11=', this.m_[0][0], ',',  
'M12=', this.m_[1][0], ',',  
'M21=', this.m_[0][1], ',',  
'M22=', this.m_[1][1], ',',  
'Dx=', mr(d.x / Z), ',',  
'Dy=', mr(d.y / Z), '');  
 
// Bounding box calculation (need to minimize displayed area so that  
// filters don't waste time on unused pixels.  
var max = d;  
var c2 = this.getCoords_(dx + dw, dy);  
var c3 = this.getCoords_(dx, dy + dh);  
var c4 = this.getCoords_(dx + dw, dy + dh);  
 
max.x = m.max(max.x, c2.x, c3.x, c4.x);  
max.y = m.max(max.y, c2.y, c3.y, c4.y);  
 
vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),  
'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',  
filter.join(''), ", sizingmethod='clip');");  
 
} else {  
vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');  
}  
 
vmlStr.push(' ">' ,  
'<g_vml_:image src="', image.src, '"',  
' style="width:', Z * dw, 'px;',  
' height:', Z * dh, 'px"',  
' cropleft="', sx / w, '"',  
' croptop="', sy / h, '"',  
' cropright="', (w - sx - sw) / w, '"',  
' cropbottom="', (h - sy - sh) / h, '"',  
' />',  
'</g_vml_:group>');  
 
this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join(''));  
};  
 
contextPrototype.stroke = function(aFill) {  
var W = 10;  
var H = 10;  
// Divide the shape into chunks if it's too long because IE has a limit  
// somewhere for how long a VML shape can be. This simple division does  
// not work with fills, only strokes, unfortunately.  
var chunkSize = 5000;  
 
var min = {x: null, y: null};  
var max = {x: null, y: null};  
 
for (var j = 0; j < this.currentPath_.length; j += chunkSize) {  
var lineStr = [];  
var lineOpen = false;  
 
lineStr.push('<g_vml_:shape',  
' filled="', !!aFill, '"',  
' style="position:absolute;width:', W, 'px;height:', H, 'px;"',  
' coordorigin="0,0"',  
' coordsize="', Z * W, ',', Z * H, '"',  
' stroked="', !aFill, '"',  
' path="');  
 
var newSeq = false;  
 
for (var i = j; i < Math.min(j + chunkSize, this.currentPath_.length); i++) {  
if (i % chunkSize == 0 && i > 0) { // move into position for next chunk  
lineStr.push(' m ', mr(this.currentPath_[i-1].x), ',', mr(this.currentPath_[i-1].y));  
}  
 
var p = this.currentPath_[i];  
var c;  
 
switch (p.type) {  
case 'moveTo':  
c = p;  
lineStr.push(' m ', mr(p.x), ',', mr(p.y));  
break;  
case 'lineTo':  
lineStr.push(' l ', mr(p.x), ',', mr(p.y));  
break;  
case 'close':  
lineStr.push(' x ');  
p = null;  
break;  
case 'bezierCurveTo':  
lineStr.push(' c ',  
mr(p.cp1x), ',', mr(p.cp1y), ',',  
mr(p.cp2x), ',', mr(p.cp2y), ',',  
mr(p.x), ',', mr(p.y));  
break;  
case 'at':  
case 'wa':  
lineStr.push(' ', p.type, ' ',  
mr(p.x - this.arcScaleX_ * p.radius), ',',  
mr(p.y - this.arcScaleY_ * p.radius), ' ',  
mr(p.x + this.arcScaleX_ * p.radius), ',',  
mr(p.y + this.arcScaleY_ * p.radius), ' ',  
mr(p.xStart), ',', mr(p.yStart), ' ',  
mr(p.xEnd), ',', mr(p.yEnd));  
break;  
}  
 
 
// TODO: Following is broken for curves due to  
// move to proper paths.  
 
// Figure out dimensions so we can do gradient fills  
// properly  
if (p) {  
if (min.x == null || p.x < min.x) {  
min.x = p.x;  
}  
if (max.x == null || p.x > max.x) {  
max.x = p.x;  
}  
if (min.y == null || p.y < min.y) {  
min.y = p.y;  
}  
if (max.y == null || p.y > max.y) {  
max.y = p.y;  
}  
}  
}  
lineStr.push(' ">');  
 
if (!aFill) {  
appendStroke(this, lineStr);  
} else {  
appendFill(this, lineStr, min, max);  
}  
 
lineStr.push('</g_vml_:shape>');  
 
this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));  
}  
};  
 
function appendStroke(ctx, lineStr) {  
var a = processStyle(ctx.strokeStyle);  
var color = a.color;  
var opacity = a.alpha * ctx.globalAlpha;  
var lineWidth = ctx.lineScale_ * ctx.lineWidth;  
 
// VML cannot correctly render a line if the width is less than 1px.  
// In that case, we dilute the color to make the line look thinner.  
if (lineWidth < 1) {  
opacity *= lineWidth;  
}  
 
lineStr.push(  
'<g_vml_:stroke',  
' opacity="', opacity, '"',  
' joinstyle="', ctx.lineJoin, '"',  
' miterlimit="', ctx.miterLimit, '"',  
' endcap="', processLineCap(ctx.lineCap), '"',  
' weight="', lineWidth, 'px"',  
' color="', color, '" />'  
);  
}  
 
function appendFill(ctx, lineStr, min, max) {  
var fillStyle = ctx.fillStyle;  
var arcScaleX = ctx.arcScaleX_;  
var arcScaleY = ctx.arcScaleY_;  
var width = max.x - min.x;  
var height = max.y - min.y;  
if (fillStyle instanceof CanvasGradient_) {  
// TODO: Gradients transformed with the transformation matrix.  
var angle = 0;  
var focus = {x: 0, y: 0};  
 
// additional offset  
var shift = 0;  
// scale factor for offset  
var expansion = 1;  
 
if (fillStyle.type_ == 'gradient') {  
var x0 = fillStyle.x0_ / arcScaleX;  
var y0 = fillStyle.y0_ / arcScaleY;  
var x1 = fillStyle.x1_ / arcScaleX;  
var y1 = fillStyle.y1_ / arcScaleY;  
var p0 = ctx.getCoords_(x0, y0);  
var p1 = ctx.getCoords_(x1, y1);  
var dx = p1.x - p0.x;  
var dy = p1.y - p0.y;  
angle = Math.atan2(dx, dy) * 180 / Math.PI;  
 
// The angle should be a non-negative number.  
if (angle < 0) {  
angle += 360;  
}  
 
// Very small angles produce an unexpected result because they are  
// converted to a scientific notation string.  
if (angle < 1e-6) {  
angle = 0;  
}  
} else {  
var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_);  
focus = {  
x: (p0.x - min.x) / width,  
y: (p0.y - min.y) / height  
};  
 
width /= arcScaleX * Z;  
height /= arcScaleY * Z;  
var dimension = m.max(width, height);  
shift = 2 * fillStyle.r0_ / dimension;  
expansion = 2 * fillStyle.r1_ / dimension - shift;  
}  
 
// We need to sort the color stops in ascending order by offset,  
// otherwise IE won't interpret it correctly.  
var stops = fillStyle.colors_;  
stops.sort(function(cs1, cs2) {  
return cs1.offset - cs2.offset;  
});  
 
var length = stops.length;  
var color1 = stops[0].color;  
var color2 = stops[length - 1].color;  
var opacity1 = stops[0].alpha * ctx.globalAlpha;  
var opacity2 = stops[length - 1].alpha * ctx.globalAlpha;  
 
var colors = [];  
for (var i = 0; i < length; i++) {  
var stop = stops[i];  
colors.push(stop.offset * expansion + shift + ' ' + stop.color);  
}  
 
// When colors attribute is used, the meanings of opacity and o:opacity2  
// are reversed.  
lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',  
' method="none" focus="100%"',  
' color="', color1, '"',  
' color2="', color2, '"',  
' colors="', colors.join(','), '"',  
' opacity="', opacity2, '"',  
' g_o_:opacity2="', opacity1, '"',  
' angle="', angle, '"',  
' focusposition="', focus.x, ',', focus.y, '" />');  
} else if (fillStyle instanceof CanvasPattern_) {  
if (width && height) {  
var deltaLeft = -min.x;  
var deltaTop = -min.y;  
lineStr.push('<g_vml_:fill',  
' position="',  
deltaLeft / width * arcScaleX * arcScaleX, ',',  
deltaTop / height * arcScaleY * arcScaleY, '"',  
' type="tile"',  
// TODO: Figure out the correct size to fit the scale.  
//' size="', w, 'px ', h, 'px"',  
' src="', fillStyle.src_, '" />');  
}  
} else {  
var a = processStyle(ctx.fillStyle);  
var color = a.color;  
var opacity = a.alpha * ctx.globalAlpha;  
lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,  
'" />');  
}  
}  
 
contextPrototype.fill = function() {  
this.stroke(true);  
};  
 
contextPrototype.closePath = function() {  
this.currentPath_.push({type: 'close'});  
};  
 
/**  
* @private  
*/  
contextPrototype.getCoords_ = function(aX, aY) {  
var m = this.m_;  
return {  
x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,  
y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2  
};  
};  
 
contextPrototype.save = function() {  
var o = {};  
copyState(this, o);  
this.aStack_.push(o);  
this.mStack_.push(this.m_);  
this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);  
};  
 
contextPrototype.restore = function() {  
if (this.aStack_.length) {  
copyState(this.aStack_.pop(), this);  
this.m_ = this.mStack_.pop();  
}  
};  
 
function matrixIsFinite(m) {  
return isFinite(m[0][0]) && isFinite(m[0][1]) &&  
isFinite(m[1][0]) && isFinite(m[1][1]) &&  
isFinite(m[2][0]) && isFinite(m[2][1]);  
}  
 
function setM(ctx, m, updateLineScale) {  
if (!matrixIsFinite(m)) {  
return;  
}  
ctx.m_ = m;  
 
if (updateLineScale) {  
// Get the line scale.  
// Determinant of this.m_ means how much the area is enlarged by the  
// transformation. So its square root can be used as a scale factor  
// for width.  
var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];  
ctx.lineScale_ = sqrt(abs(det));  
}  
}  
 
contextPrototype.translate = function(aX, aY) {  
var m1 = [  
[1, 0, 0],  
[0, 1, 0],  
[aX, aY, 1]  
];  
 
setM(this, matrixMultiply(m1, this.m_), false);  
};  
 
contextPrototype.rotate = function(aRot) {  
var c = mc(aRot);  
var s = ms(aRot);  
 
var m1 = [  
[c, s, 0],  
[-s, c, 0],  
[0, 0, 1]  
];  
 
setM(this, matrixMultiply(m1, this.m_), false);  
};  
 
contextPrototype.scale = function(aX, aY) {  
this.arcScaleX_ *= aX;  
this.arcScaleY_ *= aY;  
var m1 = [  
[aX, 0, 0],  
[0, aY, 0],  
[0, 0, 1]  
];  
 
setM(this, matrixMultiply(m1, this.m_), true);  
};  
 
contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {  
var m1 = [  
[m11, m12, 0],  
[m21, m22, 0],  
[dx, dy, 1]  
];  
 
setM(this, matrixMultiply(m1, this.m_), true);  
};  
 
contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {  
var m = [  
[m11, m12, 0],  
[m21, m22, 0],  
[dx, dy, 1]  
];  
 
setM(this, m, true);  
};  
 
/**  
* The text drawing function.  
* The maxWidth argument isn't taken in account, since no browser supports  
* it yet.  
*/  
contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) {  
var m = this.m_,  
delta = 1000,  
left = 0,  
right = delta,  
offset = {x: 0, y: 0},  
lineStr = [];  
 
var fontStyle = getComputedStyle(processFontStyle(this.font),  
this.element_);  
 
var fontStyleString = buildStyle(fontStyle);  
 
var elementStyle = this.element_.currentStyle;  
var textAlign = this.textAlign.toLowerCase();  
switch (textAlign) {  
case 'left':  
case 'center':  
case 'right':  
break;  
case 'end':  
textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left';  
break;  
case 'start':  
textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left';  
break;  
default:  
textAlign = 'left';  
}  
 
// 1.75 is an arbitrary number, as there is no info about the text baseline  
switch (this.textBaseline) {  
case 'hanging':  
case 'top':  
offset.y = fontStyle.size / 1.75;  
break;  
case 'middle':  
break;  
default:  
case null:  
case 'alphabetic':  
case 'ideographic':  
case 'bottom':  
offset.y = -fontStyle.size / 2.25;  
break;  
}  
 
switch(textAlign) {  
case 'right':  
left = delta;  
right = 0.05;  
break;  
case 'center':  
left = right = delta / 2;  
break;  
}  
 
var d = this.getCoords_(x + offset.x, y + offset.y);  
 
lineStr.push('<g_vml_:line from="', -left ,' 0" to="', right ,' 0.05" ',  
' coordsize="100 100" coordorigin="0 0"',  
' filled="', !stroke, '" stroked="', !!stroke,  
'" style="position:absolute;width:1px;height:1px;">');  
 
if (stroke) {  
appendStroke(this, lineStr);  
} else {  
// TODO: Fix the min and max params.  
appendFill(this, lineStr, {x: -left, y: 0},  
{x: right, y: fontStyle.size});  
}  
 
var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' +  
m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0';  
 
var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z);  
 
lineStr.push('<g_vml_:skew on="t" matrix="', skewM ,'" ',  
' offset="', skewOffset, '" origin="', left ,' 0" />',  
'<g_vml_:path textpathok="true" />',  
'<g_vml_:textpath on="true" string="',  
encodeHtmlAttribute(text),  
'" style="v-text-align:', textAlign,  
';font:', encodeHtmlAttribute(fontStyleString),  
'" /></g_vml_:line>');  
 
this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));  
};  
 
contextPrototype.fillText = function(text, x, y, maxWidth) {  
this.drawText_(text, x, y, maxWidth, false);  
};  
 
contextPrototype.strokeText = function(text, x, y, maxWidth) {  
this.drawText_(text, x, y, maxWidth, true);  
};  
 
contextPrototype.measureText = function(text) {  
if (!this.textMeasureEl_) {  
var s = '<span style="position:absolute;' +  
'top:-20000px;left:0;padding:0;margin:0;border:none;' +  
'white-space:pre;"></span>';  
this.element_.insertAdjacentHTML('beforeEnd', s);  
this.textMeasureEl_ = this.element_.lastChild;  
}  
var doc = this.element_.ownerDocument;  
this.textMeasureEl_.innerHTML = '';  
this.textMeasureEl_.style.font = this.font;  
// Don't use innerHTML or innerText because they allow markup/whitespace.  
this.textMeasureEl_.appendChild(doc.createTextNode(text));  
return {width: this.textMeasureEl_.offsetWidth};  
};  
 
/******** STUBS ********/  
contextPrototype.clip = function() {  
// TODO: Implement  
};  
 
contextPrototype.arcTo = function() {  
// TODO: Implement  
};  
 
contextPrototype.createPattern = function(image, repetition) {  
return new CanvasPattern_(image, repetition);  
};  
 
// Gradient / Pattern Stubs  
function CanvasGradient_(aType) {  
this.type_ = aType;  
this.x0_ = 0;  
this.y0_ = 0;  
this.r0_ = 0;  
this.x1_ = 0;  
this.y1_ = 0;  
this.r1_ = 0;  
this.colors_ = [];  
}  
 
CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {  
aColor = processStyle(aColor);  
this.colors_.push({offset: aOffset,  
color: aColor.color,  
alpha: aColor.alpha});  
};  
 
function CanvasPattern_(image, repetition) {  
assertImageIsValid(image);  
switch (repetition) {  
case 'repeat':  
case null:  
case '':  
this.repetition_ = 'repeat';  
break  
case 'repeat-x':  
case 'repeat-y':  
case 'no-repeat':  
this.repetition_ = repetition;  
break;  
default:  
throwException('SYNTAX_ERR');  
}  
 
this.src_ = image.src;  
this.width_ = image.width;  
this.height_ = image.height;  
}  
 
function throwException(s) {  
throw new DOMException_(s);  
}  
 
function assertImageIsValid(img) {  
if (!img || img.nodeType != 1 || img.tagName != 'IMG') {  
throwException('TYPE_MISMATCH_ERR');  
}  
if (img.readyState != 'complete') {  
throwException('INVALID_STATE_ERR');  
}  
}  
 
function DOMException_(s) {  
this.code = this[s];  
this.message = s +': DOM Exception ' + this.code;  
}  
var p = DOMException_.prototype = new Error;  
p.INDEX_SIZE_ERR = 1;  
p.DOMSTRING_SIZE_ERR = 2;  
p.HIERARCHY_REQUEST_ERR = 3;  
p.WRONG_DOCUMENT_ERR = 4;  
p.INVALID_CHARACTER_ERR = 5;  
p.NO_DATA_ALLOWED_ERR = 6;  
p.NO_MODIFICATION_ALLOWED_ERR = 7;  
p.NOT_FOUND_ERR = 8;  
p.NOT_SUPPORTED_ERR = 9;  
p.INUSE_ATTRIBUTE_ERR = 10;  
p.INVALID_STATE_ERR = 11;  
p.SYNTAX_ERR = 12;  
p.INVALID_MODIFICATION_ERR = 13;  
p.NAMESPACE_ERR = 14;  
p.INVALID_ACCESS_ERR = 15;  
p.VALIDATION_ERR = 16;  
p.TYPE_MISMATCH_ERR = 17;  
 
// set up externs  
G_vmlCanvasManager = G_vmlCanvasManager_;  
CanvasRenderingContext2D = CanvasRenderingContext2D_;  
CanvasGradient = CanvasGradient_;  
CanvasPattern = CanvasPattern_;  
DOMException = DOMException_;  
})();  
 
} // if  
 
/*! Javascript plotting library for jQuery, v. 0.7.  
*  
* Released under the MIT license by IOLA, December 2007.  
*  
*/  
 
// first an inline dependency, jquery.colorhelpers.js, we inline it here  
// for convenience  
 
/* Plugin for jQuery for working with colors.  
*  
* Version 1.1.  
*  
* Inspiration from jQuery color animation plugin by John Resig.  
*  
* Released under the MIT license by Ole Laursen, October 2009.  
*  
* Examples:  
*  
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()  
* var c = $.color.extract($("#mydiv"), 'background-color');  
* console.log(c.r, c.g, c.b, c.a);  
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"  
*  
* Note that .scale() and .add() return the same modified object  
* instead of making a new one.  
*  
* V. 1.1: Fix error handling so e.g. parsing an empty string does  
* produce a color rather than just crashing.  
*/  
(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);  
 
// the actual Flot code  
(function($) {  
function Plot(placeholder, data_, options_, plugins) {  
// data is on the form:  
// [ series1, series2 ... ]  
// where series is either just the data as [ [x1, y1], [x2, y2], ... ]  
// or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }  
 
var series = [],  
options = {  
// the color theme used for graphs  
colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],  
legend: {  
show: true,  
noColumns: 1, // number of colums in legend table  
labelFormatter: null, // fn: string -> string  
labelBoxBorderColor: "#ccc", // border color for the little label boxes  
container: null, // container (as jQuery object) to put legend in, null means default on top of graph  
position: "ne", // position of default legend container within plot  
margin: 5, // distance from grid edge to default legend container within plot  
backgroundColor: null, // null means auto-detect  
backgroundOpacity: 0.85 // set to 0 to avoid background  
},  
xaxis: {  
show: null, // null = auto-detect, true = always, false = never  
position: "bottom", // or "top"  
mode: null, // null or "time"  
color: null, // base color, labels, ticks  
tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"  
transform: null, // null or f: number -> number to transform axis  
inverseTransform: null, // if transform is set, this should be the inverse function  
min: null, // min. value to show, null means set automatically  
max: null, // max. value to show, null means set automatically  
autoscaleMargin: null, // margin in % to add if auto-setting min/max  
ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks  
tickFormatter: null, // fn: number -> string  
labelWidth: null, // size of tick labels in pixels  
labelHeight: null,  
reserveSpace: null, // whether to reserve space even if axis isn't shown  
tickLength: null, // size in pixels of ticks, or "full" for whole line  
alignTicksWithAxis: null, // axis number or null for no sync  
 
// mode specific options  
tickDecimals: null, // no. of decimals, null means auto  
tickSize: null, // number or [number, "unit"]  
minTickSize: null, // number or [number, "unit"]  
monthNames: null, // list of names of months  
timeformat: null, // format string to use  
twelveHourClock: false // 12 or 24 time in time mode  
},  
yaxis: {  
autoscaleMargin: 0.02,  
position: "left" // or "right"  
},  
xaxes: [],  
yaxes: [],  
series: {  
points: {  
show: false,  
radius: 3,  
lineWidth: 2, // in pixels  
fill: true,  
fillColor: "#ffffff",  
symbol: "circle" // or callback  
},  
lines: {  
// we don't put in show: false so we can see  
// whether lines were actively disabled  
lineWidth: 2, // in pixels  
fill: false,  
fillColor: null,  
steps: false  
},  
bars: {  
show: false,  
lineWidth: 2, // in pixels  
barWidth: 1, // in units of the x axis  
fill: true,  
fillColor: null,  
align: "left", // or "center"  
horizontal: false  
},  
shadowSize: 3  
},  
grid: {  
show: true,  
aboveData: false,  
color: "#545454", // primary color used for outline and labels  
backgroundColor: null, // null for transparent, else color  
borderColor: null, // set if different from the grid color  
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"  
labelMargin: 5, // in pixels  
axisMargin: 8, // in pixels  
borderWidth: 2, // in pixels  
minBorderMargin: null, // in pixels, null means taken from points radius  
markings: null, // array of ranges or fn: axes -> array of ranges  
markingsColor: "#f4f4f4",  
markingsLineWidth: 2,  
// interactive stuff  
clickable: false,  
hoverable: false,  
autoHighlight: true, // highlight in case mouse is near  
mouseActiveRadius: 10 // how far the mouse can be away to activate an item  
},  
hooks: {}  
},  
canvas = null, // the canvas for the plot itself  
overlay = null, // canvas for interactive stuff on top of plot  
eventHolder = null, // jQuery object that events should be bound to  
ctx = null, octx = null,  
xaxes = [], yaxes = [],  
plotOffset = { left: 0, right: 0, top: 0, bottom: 0},  
canvasWidth = 0, canvasHeight = 0,  
plotWidth = 0, plotHeight = 0,  
hooks = {  
processOptions: [],  
processRawData: [],  
processDatapoints: [],  
drawSeries: [],  
draw: [],  
bindEvents: [],  
drawOverlay: [],  
shutdown: []  
},  
plot = this;  
 
// public functions  
plot.setData = setData;  
plot.setupGrid = setupGrid;  
plot.draw = draw;  
plot.getPlaceholder = function() { return placeholder; };  
plot.getCanvas = function() { return canvas; };  
plot.getPlotOffset = function() { return plotOffset; };  
plot.width = function () { return plotWidth; };  
plot.height = function () { return plotHeight; };  
plot.offset = function () {  
var o = eventHolder.offset();  
o.left += plotOffset.left;  
o.top += plotOffset.top;  
return o;  
};  
plot.getData = function () { return series; };  
plot.getAxes = function () {  
var res = {}, i;  
$.each(xaxes.concat(yaxes), function (_, axis) {  
if (axis)  
res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;  
});  
return res;  
};  
plot.getXAxes = function () { return xaxes; };  
plot.getYAxes = function () { return yaxes; };  
plot.c2p = canvasToAxisCoords;  
plot.p2c = axisToCanvasCoords;  
plot.getOptions = function () { return options; };  
plot.highlight = highlight;  
plot.unhighlight = unhighlight;  
plot.triggerRedrawOverlay = triggerRedrawOverlay;  
plot.pointOffset = function(point) {  
return {  
left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),  
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)  
};  
};  
plot.shutdown = shutdown;  
plot.resize = function () {  
getCanvasDimensions();  
resizeCanvas(canvas);  
resizeCanvas(overlay);  
};  
 
// public attributes  
plot.hooks = hooks;  
 
// initialize  
initPlugins(plot);  
parseOptions(options_);  
setupCanvases();  
setData(data_);  
setupGrid();  
draw();  
bindEvents();  
 
 
function executeHooks(hook, args) {  
args = [plot].concat(args);  
for (var i = 0; i < hook.length; ++i)  
hook[i].apply(this, args);  
}  
 
function initPlugins() {  
for (var i = 0; i < plugins.length; ++i) {  
var p = plugins[i];  
p.init(plot);  
if (p.options)  
$.extend(true, options, p.options);  
}  
}  
 
function parseOptions(opts) {  
var i;  
 
$.extend(true, options, opts);  
 
if (options.xaxis.color == null)  
options.xaxis.color = options.grid.color;  
if (options.yaxis.color == null)  
options.yaxis.color = options.grid.color;  
 
if (options.xaxis.tickColor == null) // backwards-compatibility  
options.xaxis.tickColor = options.grid.tickColor;  
if (options.yaxis.tickColor == null) // backwards-compatibility  
options.yaxis.tickColor = options.grid.tickColor;  
 
if (options.grid.borderColor == null)  
options.grid.borderColor = options.grid.color;  
if (options.grid.tickColor == null)  
options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();  
 
// fill in defaults in axes, copy at least always the  
// first as the rest of the code assumes it'll be there  
for (i = 0; i < Math.max(1, options.xaxes.length); ++i)  
options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);  
for (i = 0; i < Math.max(1, options.yaxes.length); ++i)  
options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);  
 
// backwards compatibility, to be removed in future  
if (options.xaxis.noTicks && options.xaxis.ticks == null)  
options.xaxis.ticks = options.xaxis.noTicks;  
if (options.yaxis.noTicks && options.yaxis.ticks == null)  
options.yaxis.ticks = options.yaxis.noTicks;  
if (options.x2axis) {  
options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);  
options.xaxes[1].position = "top";  
}  
if (options.y2axis) {  
options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);  
options.yaxes[1].position = "right";  
}  
if (options.grid.coloredAreas)  
options.grid.markings = options.grid.coloredAreas;  
if (options.grid.coloredAreasColor)  
options.grid.markingsColor = options.grid.coloredAreasColor;  
if (options.lines)  
$.extend(true, options.series.lines, options.lines);  
if (options.points)  
$.extend(true, options.series.points, options.points);  
if (options.bars)  
$.extend(true, options.series.bars, options.bars);  
if (options.shadowSize != null)  
options.series.shadowSize = options.shadowSize;  
 
// save options on axes for future reference  
for (i = 0; i < options.xaxes.length; ++i)  
getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];  
for (i = 0; i < options.yaxes.length; ++i)  
getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];  
 
// add hooks from options  
for (var n in hooks)  
if (options.hooks[n] && options.hooks[n].length)  
hooks[n] = hooks[n].concat(options.hooks[n]);  
 
executeHooks(hooks.processOptions, [options]);  
}  
 
function setData(d) {  
series = parseData(d);  
fillInSeriesOptions();  
processData();  
}  
 
function parseData(d) {  
var res = [];  
for (var i = 0; i < d.length; ++i) {  
var s = $.extend(true, {}, options.series);  
 
if (d[i].data != null) {  
s.data = d[i].data; // move the data instead of deep-copy  
delete d[i].data;  
 
$.extend(true, s, d[i]);  
 
d[i].data = s.data;  
}  
else  
s.data = d[i];  
res.push(s);  
}  
 
return res;  
}  
 
function axisNumber(obj, coord) {  
var a = obj[coord + "axis"];  
if (typeof a == "object") // if we got a real axis, extract number  
a = a.n;  
if (typeof a != "number")  
a = 1; // default to first axis  
return a;  
}  
 
function allAxes() {  
// return flat array without annoying null entries  
return $.grep(xaxes.concat(yaxes), function (a) { return a; });  
}  
 
function canvasToAxisCoords(pos) {  
// return an object with x/y corresponding to all used axes  
var res = {}, i, axis;  
for (i = 0; i < xaxes.length; ++i) {  
axis = xaxes[i];  
if (axis && axis.used)  
res["x" + axis.n] = axis.c2p(pos.left);  
}  
 
for (i = 0; i < yaxes.length; ++i) {  
axis = yaxes[i];  
if (axis && axis.used)  
res["y" + axis.n] = axis.c2p(pos.top);  
}  
 
if (res.x1 !== undefined)  
res.x = res.x1;  
if (res.y1 !== undefined)  
res.y = res.y1;  
 
return res;  
}  
 
function axisToCanvasCoords(pos) {  
// get canvas coords from the first pair of x/y found in pos  
var res = {}, i, axis, key;  
 
for (i = 0; i < xaxes.length; ++i) {  
axis = xaxes[i];  
if (axis && axis.used) {  
key = "x" + axis.n;  
if (pos[key] == null && axis.n == 1)  
key = "x";  
 
if (pos[key] != null) {  
res.left = axis.p2c(pos[key]);  
break;  
}  
}  
}  
 
for (i = 0; i < yaxes.length; ++i) {  
axis = yaxes[i];  
if (axis && axis.used) {  
key = "y" + axis.n;  
if (pos[key] == null && axis.n == 1)  
key = "y";  
 
if (pos[key] != null) {  
res.top = axis.p2c(pos[key]);  
break;  
}  
}  
}  
 
return res;  
}  
 
function getOrCreateAxis(axes, number) {  
if (!axes[number - 1])  
axes[number - 1] = {  
n: number, // save the number for future reference  
direction: axes == xaxes ? "x" : "y",  
options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)  
};  
 
return axes[number - 1];  
}  
 
function fillInSeriesOptions() {  
var i;  
 
// collect what we already got of colors  
var neededColors = series.length,  
usedColors = [],  
assignedColors = [];  
for (i = 0; i < series.length; ++i) {  
var sc = series[i].color;  
if (sc != null) {  
--neededColors;  
if (typeof sc == "number")  
assignedColors.push(sc);  
else  
usedColors.push($.color.parse(series[i].color));  
}  
}  
 
// we might need to generate more colors if higher indices  
// are assigned  
for (i = 0; i < assignedColors.length; ++i) {  
neededColors = Math.max(neededColors, assignedColors[i] + 1);  
}  
 
// produce colors as needed  
var colors = [], variation = 0;  
i = 0;  
while (colors.length < neededColors) {  
var c;  
if (options.colors.length == i) // check degenerate case  
c = $.color.make(100, 100, 100);  
else  
c = $.color.parse(options.colors[i]);  
 
// vary color if needed  
var sign = variation % 2 == 1 ? -1 : 1;  
c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)  
 
// FIXME: if we're getting to close to something else,  
// we should probably skip this one  
colors.push(c);  
 
++i;  
if (i >= options.colors.length) {  
i = 0;  
++variation;  
}  
}  
 
// fill in the options  
var colori = 0, s;  
for (i = 0; i < series.length; ++i) {  
s = series[i];  
 
// assign colors  
if (s.color == null) {  
s.color = colors[colori].toString();  
++colori;  
}  
else if (typeof s.color == "number")  
s.color = colors[s.color].toString();  
 
// turn on lines automatically in case nothing is set  
if (s.lines.show == null) {  
var v, show = true;  
for (v in s)  
if (s[v] && s[v].show) {  
show = false;  
break;  
}  
if (show)  
s.lines.show = true;  
}  
 
// setup axes  
s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));  
s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));  
}  
}  
 
function processData() {  
var topSentry = Number.POSITIVE_INFINITY,  
bottomSentry = Number.NEGATIVE_INFINITY,  
fakeInfinity = Number.MAX_VALUE,  
i, j, k, m, length,  
s, points, ps, x, y, axis, val, f, p;  
 
function updateAxis(axis, min, max) {  
if (min < axis.datamin && min != -fakeInfinity)  
axis.datamin = min;  
if (max > axis.datamax && max != fakeInfinity)  
axis.datamax = max;  
}  
 
$.each(allAxes(), function (_, axis) {  
// init axis  
axis.datamin = topSentry;  
axis.datamax = bottomSentry;  
axis.used = false;  
});  
 
for (i = 0; i < series.length; ++i) {  
s = series[i];  
s.datapoints = { points: [] };  
 
executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);  
}  
 
// first pass: clean and copy data  
for (i = 0; i < series.length; ++i) {  
s = series[i];  
 
var data = s.data, format = s.datapoints.format;  
 
if (!format) {  
format = [];  
// find out how to copy  
format.push({ x: true, number: true, required: true });  
format.push({ y: true, number: true, required: true });  
 
if (s.bars.show || (s.lines.show && s.lines.fill)) {  
format.push({ y: true, number: true, required: false, defaultValue: 0 });  
if (s.bars.horizontal) {  
delete format[format.length - 1].y;  
format[format.length - 1].x = true;  
}  
}  
 
s.datapoints.format = format;  
}  
 
if (s.datapoints.pointsize != null)  
continue; // already filled in  
 
s.datapoints.pointsize = format.length;  
 
ps = s.datapoints.pointsize;  
points = s.datapoints.points;  
 
insertSteps = s.lines.show && s.lines.steps;  
s.xaxis.used = s.yaxis.used = true;  
 
for (j = k = 0; j < data.length; ++j, k += ps) {  
p = data[j];  
 
var nullify = p == null;  
if (!nullify) {  
for (m = 0; m < ps; ++m) {  
val = p[m];  
f = format[m];  
 
if (f) {  
if (f.number && val != null) {  
val = +val; // convert to number  
if (isNaN(val))  
val = null;  
else if (val == Infinity)  
val = fakeInfinity;  
else if (val == -Infinity)  
val = -fakeInfinity;  
}  
 
if (val == null) {  
if (f.required)  
nullify = true;  
 
if (f.defaultValue != null)  
val = f.defaultValue;  
}  
}  
 
points[k + m] = val;  
}  
}  
 
if (nullify) {  
for (m = 0; m < ps; ++m) {  
val = points[k + m];  
if (val != null) {  
f = format[m];  
// extract min/max info  
if (f.x)  
updateAxis(s.xaxis, val, val);  
if (f.y)  
updateAxis(s.yaxis, val, val);  
}  
points[k + m] = null;  
}  
}  
else {  
// a little bit of line specific stuff that  
// perhaps shouldn't be here, but lacking  
// better means...  
if (insertSteps && k > 0  
&& points[k - ps] != null  
&& points[k - ps] != points[k]  
&& points[k - ps + 1] != points[k + 1]) {  
// copy the point to make room for a middle point  
for (m = 0; m < ps; ++m)  
points[k + ps + m] = points[k + m];  
 
// middle point has same y  
points[k + 1] = points[k - ps + 1];  
 
// we've added a point, better reflect that  
k += ps;  
}  
}  
}  
}  
 
// give the hooks a chance to run  
for (i = 0; i < series.length; ++i) {  
s = series[i];  
 
executeHooks(hooks.processDatapoints, [ s, s.datapoints]);  
}  
 
// second pass: find datamax/datamin for auto-scaling  
for (i = 0; i < series.length; ++i) {  
s = series[i];  
points = s.datapoints.points,  
ps = s.datapoints.pointsize;  
 
var xmin = topSentry, ymin = topSentry,  
xmax = bottomSentry, ymax = bottomSentry;  
 
for (j = 0; j < points.length; j += ps) {  
if (points[j] == null)  
continue;  
 
for (m = 0; m < ps; ++m) {  
val = points[j + m];  
f = format[m];  
if (!f || val == fakeInfinity || val == -fakeInfinity)  
continue;  
 
if (f.x) {  
if (val < xmin)  
xmin = val;  
if (val > xmax)  
xmax = val;  
}  
if (f.y) {  
if (val < ymin)  
ymin = val;  
if (val > ymax)  
ymax = val;  
}  
}  
}  
 
if (s.bars.show) {  
// make sure we got room for the bar on the dancing floor  
var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;  
if (s.bars.horizontal) {  
ymin += delta;  
ymax += delta + s.bars.barWidth;  
}  
else {  
xmin += delta;  
xmax += delta + s.bars.barWidth;  
}  
}  
 
updateAxis(s.xaxis, xmin, xmax);  
updateAxis(s.yaxis, ymin, ymax);  
}  
 
$.each(allAxes(), function (_, axis) {  
if (axis.datamin == topSentry)  
axis.datamin = null;  
if (axis.datamax == bottomSentry)  
axis.datamax = null;  
});  
}  
 
function makeCanvas(skipPositioning, cls) {  
var c = document.createElement('canvas');  
c.className = cls;  
c.width = canvasWidth;  
c.height = canvasHeight;  
 
if (!skipPositioning)  
$(c).css({ position: 'absolute', left: 0, top: 0 });  
 
$(c).appendTo(placeholder);  
 
if (!c.getContext) // excanvas hack  
c = window.G_vmlCanvasManager.initElement(c);  
 
// used for resetting in case we get replotted  
c.getContext("2d").save();  
 
return c;  
}  
 
function getCanvasDimensions() {  
canvasWidth = placeholder.width();  
canvasHeight = placeholder.height();  
 
if (canvasWidth <= 0 || canvasHeight <= 0)  
throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;  
}  
 
function resizeCanvas(c) {  
// resizing should reset the state (excanvas seems to be  
// buggy though)  
if (c.width != canvasWidth)  
c.width = canvasWidth;  
 
if (c.height != canvasHeight)  
c.height = canvasHeight;  
 
// so try to get back to the initial state (even if it's  
// gone now, this should be safe according to the spec)  
var cctx = c.getContext("2d");  
cctx.restore();  
 
// and save again  
cctx.save();  
}  
 
function setupCanvases() {  
var reused,  
existingCanvas = placeholder.children("canvas.base"),  
existingOverlay = placeholder.children("canvas.overlay");  
 
if (existingCanvas.length == 0 || existingOverlay == 0) {  
// init everything  
 
placeholder.html(""); // make sure placeholder is clear  
 
placeholder.css({ padding: 0 }); // padding messes up the positioning  
 
if (placeholder.css("position") == 'static')  
placeholder.css("position", "relative"); // for positioning labels and overlay  
 
getCanvasDimensions();  
 
canvas = makeCanvas(true, "base");  
overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features  
 
reused = false;  
}  
else {  
// reuse existing elements  
 
canvas = existingCanvas.get(0);  
overlay = existingOverlay.get(0);  
 
reused = true;  
}  
 
ctx = canvas.getContext("2d");  
octx = overlay.getContext("2d");  
 
// we include the canvas in the event holder too, because IE 7  
// sometimes has trouble with the stacking order  
eventHolder = $([overlay, canvas]);  
 
if (reused) {  
// run shutdown in the old plot object  
placeholder.data("plot").shutdown();  
 
// reset reused canvases  
plot.resize();  
 
// make sure overlay pixels are cleared (canvas is cleared when we redraw)  
octx.clearRect(0, 0, canvasWidth, canvasHeight);  
 
// then whack any remaining obvious garbage left  
eventHolder.unbind();  
placeholder.children().not([canvas, overlay]).remove();  
}  
 
// save in case we get replotted  
placeholder.data("plot", plot);  
}  
 
function bindEvents() {  
// bind events  
if (options.grid.hoverable) {  
eventHolder.mousemove(onMouseMove);  
eventHolder.mouseleave(onMouseLeave);  
}  
 
if (options.grid.clickable)  
eventHolder.click(onClick);  
 
executeHooks(hooks.bindEvents, [eventHolder]);  
}  
 
function shutdown() {  
if (redrawTimeout)  
clearTimeout(redrawTimeout);  
 
eventHolder.unbind("mousemove", onMouseMove);  
eventHolder.unbind("mouseleave", onMouseLeave);  
eventHolder.unbind("click", onClick);  
 
executeHooks(hooks.shutdown, [eventHolder]);  
}  
 
function setTransformationHelpers(axis) {  
// set helper functions on the axis, assumes plot area  
// has been computed already  
 
function identity(x) { return x; }  
 
var s, m, t = axis.options.transform || identity,  
it = axis.options.inverseTransform;  
 
// precompute how much the axis is scaling a point  
// in canvas space  
if (axis.direction == "x") {  
s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));  
m = Math.min(t(axis.max), t(axis.min));  
}  
else {  
s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));  
s = -s;  
m = Math.max(t(axis.max), t(axis.min));  
}  
 
// data point to canvas coordinate  
if (t == identity) // slight optimization  
axis.p2c = function (p) { return (p - m) * s; };  
else  
axis.p2c = function (p) { return (t(p) - m) * s; };  
// canvas coordinate to data point  
if (!it)  
axis.c2p = function (c) { return m + c / s; };  
else  
axis.c2p = function (c) { return it(m + c / s); };  
}  
 
function measureTickLabels(axis) {  
var opts = axis.options, i, ticks = axis.ticks || [], labels = [],  
l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;  
 
function makeDummyDiv(labels, width) {  
return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +  
'<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'  
+ labels.join("") + '</div></div>')  
.appendTo(placeholder);  
}  
 
if (axis.direction == "x") {  
// to avoid measuring the widths of the labels (it's slow), we  
// construct fixed-size boxes and put the labels inside  
// them, we don't need the exact figures and the  
// fixed-size box content is easy to center  
if (w == null)  
w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));  
 
// measure x label heights  
if (h == null) {  
labels = [];  
for (i = 0; i < ticks.length; ++i) {  
l = ticks[i].label;  
if (l)  
labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');  
}  
 
if (labels.length > 0) {  
// stick them all in the same div and measure  
// collective height  
labels.push('<div style="clear:left"></div>');  
dummyDiv = makeDummyDiv(labels, "width:10000px;");  
h = dummyDiv.height();  
dummyDiv.remove();  
}  
}  
}  
else if (w == null || h == null) {  
// calculate y label dimensions  
for (i = 0; i < ticks.length; ++i) {  
l = ticks[i].label;  
if (l)  
labels.push('<div class="tickLabel">' + l + '</div>');  
}  
 
if (labels.length > 0) {  
dummyDiv = makeDummyDiv(labels, "");  
if (w == null)  
w = dummyDiv.children().width();  
if (h == null)  
h = dummyDiv.find("div.tickLabel").height();  
dummyDiv.remove();  
}  
}  
 
if (w == null)  
w = 0;  
if (h == null)  
h = 0;  
 
axis.labelWidth = w;  
axis.labelHeight = h;  
}  
 
function allocateAxisBoxFirstPhase(axis) {  
// find the bounding box of the axis by looking at label  
// widths/heights and ticks, make room by diminishing the  
// plotOffset  
 
var lw = axis.labelWidth,  
lh = axis.labelHeight,  
pos = axis.options.position,  
tickLength = axis.options.tickLength,  
axismargin = options.grid.axisMargin,  
padding = options.grid.labelMargin,  
all = axis.direction == "x" ? xaxes : yaxes,  
index;  
 
// determine axis margin  
var samePosition = $.grep(all, function (a) {  
return a && a.options.position == pos && a.reserveSpace;  
});  
if ($.inArray(axis, samePosition) == samePosition.length - 1)  
axismargin = 0; // outermost  
 
// determine tick length - if we're innermost, we can use "full"  
if (tickLength == null)  
tickLength = "full";  
 
var sameDirection = $.grep(all, function (a) {  
return a && a.reserveSpace;  
});  
 
var innermost = $.inArray(axis, sameDirection) == 0;  
if (!innermost && tickLength == "full")  
tickLength = 5;  
 
if (!isNaN(+tickLength))  
padding += +tickLength;  
 
// compute box  
if (axis.direction == "x") {  
lh += padding;  
 
if (pos == "bottom") {  
plotOffset.bottom += lh + axismargin;  
axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };  
}  
else {  
axis.box = { top: plotOffset.top + axismargin, height: lh };  
plotOffset.top += lh + axismargin;  
}  
}  
else {  
lw += padding;  
 
if (pos == "left") {  
axis.box = { left: plotOffset.left + axismargin, width: lw };  
plotOffset.left += lw + axismargin;  
}  
else {  
plotOffset.right += lw + axismargin;  
axis.box = { left: canvasWidth - plotOffset.right, width: lw };  
}  
}  
 
// save for future reference  
axis.position = pos;  
axis.tickLength = tickLength;  
axis.box.padding = padding;  
axis.innermost = innermost;  
}  
 
function allocateAxisBoxSecondPhase(axis) {  
// set remaining bounding box coordinates  
if (axis.direction == "x") {  
axis.box.left = plotOffset.left;  
axis.box.width = plotWidth;  
}  
else {  
axis.box.top = plotOffset.top;  
axis.box.height = plotHeight;  
}  
}  
 
function setupGrid() {  
var i, axes = allAxes();  
 
// first calculate the plot and axis box dimensions  
 
$.each(axes, function (_, axis) {  
axis.show = axis.options.show;  
if (axis.show == null)  
axis.show = axis.used; // by default an axis is visible if it's got data  
 
axis.reserveSpace = axis.show || axis.options.reserveSpace;  
 
setRange(axis);  
});  
 
allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });  
 
plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;  
if (options.grid.show) {  
$.each(allocatedAxes, function (_, axis) {  
// make the ticks  
setupTickGeneration(axis);  
setTicks(axis);  
snapRangeToTicks(axis, axis.ticks);  
 
// find labelWidth/Height for axis  
measureTickLabels(axis);  
});  
 
// with all dimensions in house, we can compute the  
// axis boxes, start from the outside (reverse order)  
for (i = allocatedAxes.length - 1; i >= 0; --i)  
allocateAxisBoxFirstPhase(allocatedAxes[i]);  
 
// make sure we've got enough space for things that  
// might stick out  
var minMargin = options.grid.minBorderMargin;  
if (minMargin == null) {  
minMargin = 0;  
for (i = 0; i < series.length; ++i)  
minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);  
}  
 
for (var a in plotOffset) {  
plotOffset[a] += options.grid.borderWidth;  
plotOffset[a] = Math.max(minMargin, plotOffset[a]);  
}  
}  
 
plotWidth = canvasWidth - plotOffset.left - plotOffset.right;  
plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;  
 
// now we got the proper plotWidth/Height, we can compute the scaling  
$.each(axes, function (_, axis) {  
setTransformationHelpers(axis);  
});  
 
if (options.grid.show) {  
$.each(allocatedAxes, function (_, axis) {  
allocateAxisBoxSecondPhase(axis);  
});  
 
insertAxisLabels();  
}  
 
insertLegend();  
}  
 
function setRange(axis) {  
var opts = axis.options,  
min = +(opts.min != null ? opts.min : axis.datamin),  
max = +(opts.max != null ? opts.max : axis.datamax),  
delta = max - min;  
 
if (delta == 0.0) {  
// degenerate case  
var widen = max == 0 ? 1 : 0.01;  
 
if (opts.min == null)  
min -= widen;  
// always widen max if we couldn't widen min to ensure we  
// don't fall into min == max which doesn't work  
if (opts.max == null || opts.min != null)  
max += widen;  
}  
else {  
// consider autoscaling  
var margin = opts.autoscaleMargin;  
if (margin != null) {  
if (opts.min == null) {  
min -= delta * margin;  
// make sure we don't go below zero if all values  
// are positive  
if (min < 0 && axis.datamin != null && axis.datamin >= 0)  
min = 0;  
}  
if (opts.max == null) {  
max += delta * margin;  
if (max > 0 && axis.datamax != null && axis.datamax <= 0)  
max = 0;  
}  
}  
}  
axis.min = min;  
axis.max = max;  
}  
 
function setupTickGeneration(axis) {  
var opts = axis.options;  
 
// estimate number of ticks  
var noTicks;  
if (typeof opts.ticks == "number" && opts.ticks > 0)  
noTicks = opts.ticks;  
else  
// heuristic based on the model a*sqrt(x) fitted to  
// some data points that seemed reasonable  
noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);  
 
var delta = (axis.max - axis.min) / noTicks,  
size, generator, unit, formatter, i, magn, norm;  
 
if (opts.mode == "time") {  
// pretty handling of time  
 
// map of app. size of time units in milliseconds  
var timeUnitSize = {  
"second": 1000,  
"minute": 60 * 1000,  
"hour": 60 * 60 * 1000,  
"day": 24 * 60 * 60 * 1000,  
"month": 30 * 24 * 60 * 60 * 1000,  
"year": 365.2425 * 24 * 60 * 60 * 1000  
};  
 
 
// the allowed tick sizes, after 1 year we use  
// an integer algorithm  
var spec = [  
[1, "second"], [2, "second"], [5, "second"], [10, "second"],  
[30, "second"],  
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],  
[30, "minute"],  
[1, "hour"], [2, "hour"], [4, "hour"],  
[8, "hour"], [12, "hour"],  
[1, "day"], [2, "day"], [3, "day"],  
[0.25, "month"], [0.5, "month"], [1, "month"],  
[2, "month"], [3, "month"], [6, "month"],  
[1, "year"]  
];  
 
var minSize = 0;  
if (opts.minTickSize != null) {  
if (typeof opts.tickSize == "number")  
minSize = opts.tickSize;  
else  
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];  
}  
 
for (var i = 0; i < spec.length - 1; ++i)  
if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]  
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2  
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)  
break;  
size = spec[i][0];  
unit = spec[i][1];  
 
// special-case the possibility of several years  
if (unit == "year") {  
magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));  
norm = (delta / timeUnitSize.year) / magn;  
if (norm < 1.5)  
size = 1;  
else if (norm < 3)  
size = 2;  
else if (norm < 7.5)  
size = 5;  
else  
size = 10;  
 
size *= magn;  
}  
 
axis.tickSize = opts.tickSize || [size, unit];  
 
generator = function(axis) {  
var ticks = [],  
tickSize = axis.tickSize[0], unit = axis.tickSize[1],  
d = new Date(axis.min);  
 
var step = tickSize * timeUnitSize[unit];  
 
if (unit == "second")  
d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));  
if (unit == "minute")  
d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));  
if (unit == "hour")  
d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));  
if (unit == "month")  
d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));  
if (unit == "year")  
d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));  
 
// reset smaller components  
d.setUTCMilliseconds(0);  
if (step >= timeUnitSize.minute)  
d.setUTCSeconds(0);  
if (step >= timeUnitSize.hour)  
d.setUTCMinutes(0);  
if (step >= timeUnitSize.day)  
d.setUTCHours(0);  
if (step >= timeUnitSize.day * 4)  
d.setUTCDate(1);  
if (step >= timeUnitSize.year)  
d.setUTCMonth(0);  
 
 
var carry = 0, v = Number.NaN, prev;  
do {  
prev = v;  
v = d.getTime();  
ticks.push(v);  
if (unit == "month") {  
if (tickSize < 1) {  
// a bit complicated - we'll divide the month  
// up but we need to take care of fractions  
// so we don't end up in the middle of a day  
d.setUTCDate(1);  
var start = d.getTime();  
d.setUTCMonth(d.getUTCMonth() + 1);  
var end = d.getTime();  
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);  
carry = d.getUTCHours();  
d.setUTCHours(0);  
}  
else  
d.setUTCMonth(d.getUTCMonth() + tickSize);  
}  
else if (unit == "year") {  
d.setUTCFullYear(d.getUTCFullYear() + tickSize);  
}  
else  
d.setTime(v + step);  
} while (v < axis.max && v != prev);  
 
return ticks;  
};  
 
formatter = function (v, axis) {  
var d = new Date(v);  
 
// first check global format  
if (opts.timeformat != null)  
return $.plot.formatDate(d, opts.timeformat, opts.monthNames);  
 
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];  
var span = axis.max - axis.min;  
var suffix = (opts.twelveHourClock) ? " %p" : "";  
 
if (t < timeUnitSize.minute)  
fmt = "%h:%M:%S" + suffix;  
else if (t < timeUnitSize.day) {  
if (span < 2 * timeUnitSize.day)  
fmt = "%h:%M" + suffix;  
else  
fmt = "%b %d %h:%M" + suffix;  
}  
else if (t < timeUnitSize.month)  
fmt = "%b %d";  
else if (t < timeUnitSize.year) {  
if (span < timeUnitSize.year)  
fmt = "%b";  
else  
fmt = "%b %y";  
}  
else  
fmt = "%y";  
 
return $.plot.formatDate(d, fmt, opts.monthNames);  
};  
}  
else {  
// pretty rounding of base-10 numbers  
var maxDec = opts.tickDecimals;  
var dec = -Math.floor(Math.log(delta) / Math.LN10);  
if (maxDec != null && dec > maxDec)  
dec = maxDec;  
 
magn = Math.pow(10, -dec);  
norm = delta / magn; // norm is between 1.0 and 10.0  
 
if (norm < 1.5)  
size = 1;  
else if (norm < 3) {  
size = 2;  
// special case for 2.5, requires an extra decimal  
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {  
size = 2.5;  
++dec;  
}  
}  
else if (norm < 7.5)  
size = 5;  
else  
size = 10;  
 
size *= magn;  
 
if (opts.minTickSize != null && size < opts.minTickSize)  
size = opts.minTickSize;  
 
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);  
axis.tickSize = opts.tickSize || size;  
 
generator = function (axis) {  
var ticks = [];  
 
// spew out all possible ticks  
var start = floorInBase(axis.min, axis.tickSize),  
i = 0, v = Number.NaN, prev;  
do {  
prev = v;  
v = start + i * axis.tickSize;  
ticks.push(v);  
++i;  
} while (v < axis.max && v != prev);  
return ticks;  
};  
 
formatter = function (v, axis) {  
return v.toFixed(axis.tickDecimals);  
};  
}  
 
if (opts.alignTicksWithAxis != null) {  
var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];  
if (otherAxis && otherAxis.used && otherAxis != axis) {  
// consider snapping min/max to outermost nice ticks  
var niceTicks = generator(axis);  
if (niceTicks.length > 0) {  
if (opts.min == null)  
axis.min = Math.min(axis.min, niceTicks[0]);  
if (opts.max == null && niceTicks.length > 1)  
axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);  
}  
 
generator = function (axis) {  
// copy ticks, scaled to this axis  
var ticks = [], v, i;  
for (i = 0; i < otherAxis.ticks.length; ++i) {  
v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);  
v = axis.min + v * (axis.max - axis.min);  
ticks.push(v);  
}  
return ticks;  
};  
 
// we might need an extra decimal since forced  
// ticks don't necessarily fit naturally  
if (axis.mode != "time" && opts.tickDecimals == null) {  
var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),  
ts = generator(axis);  
 
// only proceed if the tick interval rounded  
// with an extra decimal doesn't give us a  
// zero at end  
if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))  
axis.tickDecimals = extraDec;  
}  
}  
}  
 
axis.tickGenerator = generator;  
if ($.isFunction(opts.tickFormatter))  
axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };  
else  
axis.tickFormatter = formatter;  
}  
 
function setTicks(axis) {  
var oticks = axis.options.ticks, ticks = [];  
if (oticks == null || (typeof oticks == "number" && oticks > 0))  
ticks = axis.tickGenerator(axis);  
else if (oticks) {  
if ($.isFunction(oticks))  
// generate the ticks  
ticks = oticks({ min: axis.min, max: axis.max });  
else  
ticks = oticks;  
}  
 
// clean up/labelify the supplied ticks, copy them over  
var i, v;  
axis.ticks = [];  
for (i = 0; i < ticks.length; ++i) {  
var label = null;  
var t = ticks[i];  
if (typeof t == "object") {  
v = +t[0];  
if (t.length > 1)  
label = t[1];  
}  
else  
v = +t;  
if (label == null)  
label = axis.tickFormatter(v, axis);  
if (!isNaN(v))  
axis.ticks.push({ v: v, label: label });  
}  
}  
 
function snapRangeToTicks(axis, ticks) {  
if (axis.options.autoscaleMargin && ticks.length > 0) {  
// snap to ticks  
if (axis.options.min == null)  
axis.min = Math.min(axis.min, ticks[0].v);  
if (axis.options.max == null && ticks.length > 1)  
axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);  
}  
}  
 
function draw() {  
ctx.clearRect(0, 0, canvasWidth, canvasHeight);  
 
var grid = options.grid;  
 
// draw background, if any  
if (grid.show && grid.backgroundColor)  
drawBackground();  
 
if (grid.show && !grid.aboveData)  
drawGrid();  
 
for (var i = 0; i < series.length; ++i) {  
executeHooks(hooks.drawSeries, [ctx, series[i]]);  
drawSeries(series[i]);  
}  
 
executeHooks(hooks.draw, [ctx]);  
 
if (grid.show && grid.aboveData)  
drawGrid();  
}  
 
function extractRange(ranges, coord) {  
var axis, from, to, key, axes = allAxes();  
 
for (i = 0; i < axes.length; ++i) {  
axis = axes[i];  
if (axis.direction == coord) {  
key = coord + axis.n + "axis";  
if (!ranges[key] && axis.n == 1)  
key = coord + "axis"; // support x1axis as xaxis  
if (ranges[key]) {  
from = ranges[key].from;  
to = ranges[key].to;  
break;  
}  
}  
}  
 
// backwards-compat stuff - to be removed in future  
if (!ranges[key]) {  
axis = coord == "x" ? xaxes[0] : yaxes[0];  
from = ranges[coord + "1"];  
to = ranges[coord + "2"];  
}  
 
// auto-reverse as an added bonus  
if (from != null && to != null && from > to) {  
var tmp = from;  
from = to;  
to = tmp;  
}  
 
return { from: from, to: to, axis: axis };  
}  
 
function drawBackground() {  
ctx.save();  
ctx.translate(plotOffset.left, plotOffset.top);  
 
ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");  
ctx.fillRect(0, 0, plotWidth, plotHeight);  
ctx.restore();  
}  
 
function drawGrid() {  
var i;  
 
ctx.save();  
ctx.translate(plotOffset.left, plotOffset.top);  
 
// draw markings  
var markings = options.grid.markings;  
if (markings) {  
if ($.isFunction(markings)) {  
var axes = plot.getAxes();  
// xmin etc. is backwards compatibility, to be  
// removed in the future  
axes.xmin = axes.xaxis.min;  
axes.xmax = axes.xaxis.max;  
axes.ymin = axes.yaxis.min;  
axes.ymax = axes.yaxis.max;  
 
markings = markings(axes);  
}  
 
for (i = 0; i < markings.length; ++i) {  
var m = markings[i],  
xrange = extractRange(m, "x"),  
yrange = extractRange(m, "y");  
 
// fill in missing  
if (xrange.from == null)  
xrange.from = xrange.axis.min;  
if (xrange.to == null)  
xrange.to = xrange.axis.max;  
if (yrange.from == null)  
yrange.from = yrange.axis.min;  
if (yrange.to == null)  
yrange.to = yrange.axis.max;  
 
// clip  
if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||  
yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)  
continue;  
 
xrange.from = Math.max(xrange.from, xrange.axis.min);  
xrange.to = Math.min(xrange.to, xrange.axis.max);  
yrange.from = Math.max(yrange.from, yrange.axis.min);  
yrange.to = Math.min(yrange.to, yrange.axis.max);  
 
if (xrange.from == xrange.to && yrange.from == yrange.to)  
continue;  
 
// then draw  
xrange.from = xrange.axis.p2c(xrange.from);  
xrange.to = xrange.axis.p2c(xrange.to);  
yrange.from = yrange.axis.p2c(yrange.from);  
yrange.to = yrange.axis.p2c(yrange.to);  
 
if (xrange.from == xrange.to || yrange.from == yrange.to) {  
// draw line  
ctx.beginPath();  
ctx.strokeStyle = m.color || options.grid.markingsColor;  
ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;  
ctx.moveTo(xrange.from, yrange.from);  
ctx.lineTo(xrange.to, yrange.to);  
ctx.stroke();  
}  
else {  
// fill area  
ctx.fillStyle = m.color || options.grid.markingsColor;  
ctx.fillRect(xrange.from, yrange.to,  
xrange.to - xrange.from,  
yrange.from - yrange.to);  
}  
}  
}  
 
// draw the ticks  
var axes = allAxes(), bw = options.grid.borderWidth;  
 
for (var j = 0; j < axes.length; ++j) {  
var axis = axes[j], box = axis.box,  
t = axis.tickLength, x, y, xoff, yoff;  
if (!axis.show || axis.ticks.length == 0)  
continue  
 
ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();  
ctx.lineWidth = 1;  
 
// find the edges  
if (axis.direction == "x") {  
x = 0;  
if (t == "full")  
y = (axis.position == "top" ? 0 : plotHeight);  
else  
y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);  
}  
else {  
y = 0;  
if (t == "full")  
x = (axis.position == "left" ? 0 : plotWidth);  
else  
x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);  
}  
 
// draw tick bar  
if (!axis.innermost) {  
ctx.beginPath();  
xoff = yoff = 0;  
if (axis.direction == "x")  
xoff = plotWidth;  
else  
yoff = plotHeight;  
 
if (ctx.lineWidth == 1) {  
x = Math.floor(x) + 0.5;  
y = Math.floor(y) + 0.5;  
}  
 
ctx.moveTo(x, y);  
ctx.lineTo(x + xoff, y + yoff);  
ctx.stroke();  
}  
 
// draw ticks  
ctx.beginPath();  
for (i = 0; i < axis.ticks.length; ++i) {  
var v = axis.ticks[i].v;  
 
xoff = yoff = 0;  
 
if (v < axis.min || v > axis.max  
// skip those lying on the axes if we got a border  
|| (t == "full" && bw > 0  
&& (v == axis.min || v == axis.max)))  
continue;  
 
if (axis.direction == "x") {  
x = axis.p2c(v);  
yoff = t == "full" ? -plotHeight : t;  
 
if (axis.position == "top")  
yoff = -yoff;  
}  
else {  
y = axis.p2c(v);  
xoff = t == "full" ? -plotWidth : t;  
 
if (axis.position == "left")  
xoff = -xoff;  
}  
 
if (ctx.lineWidth == 1) {  
if (axis.direction == "x")  
x = Math.floor(x) + 0.5;  
else  
y = Math.floor(y) + 0.5;  
}  
 
ctx.moveTo(x, y);  
ctx.lineTo(x + xoff, y + yoff);  
}  
 
ctx.stroke();  
}  
 
 
// draw border  
if (bw) {  
ctx.lineWidth = bw;  
ctx.strokeStyle = options.grid.borderColor;  
ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);  
}  
 
ctx.restore();  
}  
 
function insertAxisLabels() {  
placeholder.find(".tickLabels").remove();  
 
var html = ['<div class="tickLabels" style="font-size:smaller">'];  
 
var axes = allAxes();  
for (var j = 0; j < axes.length; ++j) {  
var axis = axes[j], box = axis.box;  
if (!axis.show)  
continue;  
//debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>')  
html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');  
for (var i = 0; i < axis.ticks.length; ++i) {  
var tick = axis.ticks[i];  
if (!tick.label || tick.v < axis.min || tick.v > axis.max)  
continue;  
 
var pos = {}, align;  
 
if (axis.direction == "x") {  
align = "center";  
pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);  
if (axis.position == "bottom")  
pos.top = box.top + box.padding;  
else  
pos.bottom = canvasHeight - (box.top + box.height - box.padding);  
}  
else {  
pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);  
if (axis.position == "left") {  
pos.right = canvasWidth - (box.left + box.width - box.padding)  
align = "right";  
}  
else {  
pos.left = box.left + box.padding;  
align = "left";  
}  
}  
 
pos.width = axis.labelWidth;  
 
var style = ["position:absolute", "text-align:" + align ];  
for (var a in pos)  
style.push(a + ":" + pos[a] + "px")  
 
html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');  
}  
html.push('</div>');  
}  
 
html.push('</div>');  
 
placeholder.append(html.join(""));  
}  
 
function drawSeries(series) {  
if (series.lines.show)  
drawSeriesLines(series);  
if (series.bars.show)  
drawSeriesBars(series);  
if (series.points.show)  
drawSeriesPoints(series);  
}  
 
function drawSeriesLines(series) {  
function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {  
var points = datapoints.points,  
ps = datapoints.pointsize,  
prevx = null, prevy = null;  
 
ctx.beginPath();  
for (var i = ps; i < points.length; i += ps) {  
var x1 = points[i - ps], y1 = points[i - ps + 1],  
x2 = points[i], y2 = points[i + 1];  
 
if (x1 == null || x2 == null)  
continue;  
 
// clip with ymin  
if (y1 <= y2 && y1 < axisy.min) {  
if (y2 < axisy.min)  
continue; // line segment is outside  
// compute new intersection point  
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;  
y1 = axisy.min;  
}  
else if (y2 <= y1 && y2 < axisy.min) {  
if (y1 < axisy.min)  
continue;  
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;  
y2 = axisy.min;  
}  
 
// clip with ymax  
if (y1 >= y2 && y1 > axisy.max) {  
if (y2 > axisy.max)  
continue;  
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;  
y1 = axisy.max;  
}  
else if (y2 >= y1 && y2 > axisy.max) {  
if (y1 > axisy.max)  
continue;  
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;  
y2 = axisy.max;  
}  
 
// clip with xmin  
if (x1 <= x2 && x1 < axisx.min) {  
if (x2 < axisx.min)  
continue;  
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;  
x1 = axisx.min;  
}  
else if (x2 <= x1 && x2 < axisx.min) {  
if (x1 < axisx.min)  
continue;  
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;  
x2 = axisx.min;  
}  
 
// clip with xmax  
if (x1 >= x2 && x1 > axisx.max) {  
if (x2 > axisx.max)  
continue;  
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;  
x1 = axisx.max;  
}  
else if (x2 >= x1 && x2 > axisx.max) {  
if (x1 > axisx.max)  
continue;  
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;  
x2 = axisx.max;  
}  
 
if (x1 != prevx || y1 != prevy)  
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);  
 
prevx = x2;  
prevy = y2;  
ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);  
}  
ctx.stroke();  
}  
 
function plotLineArea(datapoints, axisx, axisy) {  
var points = datapoints.points,  
ps = datapoints.pointsize,  
bottom = Math.min(Math.max(0, axisy.min), axisy.max),  
i = 0, top, areaOpen = false,  
ypos = 1, segmentStart = 0, segmentEnd = 0;  
 
// we process each segment in two turns, first forward  
// direction to sketch out top, then once we hit the  
// end we go backwards to sketch the bottom  
while (true) {  
if (ps > 0 && i > points.length + ps)  
break;  
 
i += ps; // ps is negative if going backwards  
 
var x1 = points[i - ps],  
y1 = points[i - ps + ypos],  
x2 = points[i], y2 = points[i + ypos];  
 
if (areaOpen) {  
if (ps > 0 && x1 != null && x2 == null) {  
// at turning point  
segmentEnd = i;  
ps = -ps;  
ypos = 2;  
continue;  
}  
 
if (ps < 0 && i == segmentStart + ps) {  
// done with the reverse sweep  
ctx.fill();  
areaOpen = false;  
ps = -ps;  
ypos = 1;  
i = segmentStart = segmentEnd + ps;  
continue;  
}  
}  
 
if (x1 == null || x2 == null)  
continue;  
 
// clip x values  
 
// clip with xmin  
if (x1 <= x2 && x1 < axisx.min) {  
if (x2 < axisx.min)  
continue;  
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;  
x1 = axisx.min;  
}  
else if (x2 <= x1 && x2 < axisx.min) {  
if (x1 < axisx.min)  
continue;  
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;  
x2 = axisx.min;  
}  
 
// clip with xmax  
if (x1 >= x2 && x1 > axisx.max) {  
if (x2 > axisx.max)  
continue;  
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;  
x1 = axisx.max;  
}  
else if (x2 >= x1 && x2 > axisx.max) {  
if (x1 > axisx.max)  
continue;  
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;  
x2 = axisx.max;  
}  
 
if (!areaOpen) {  
// open area  
ctx.beginPath();  
ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));  
areaOpen = true;  
}  
 
// now first check the case where both is outside  
if (y1 >= axisy.max && y2 >= axisy.max) {  
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));  
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));  
continue;  
}  
else if (y1 <= axisy.min && y2 <= axisy.min) {  
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));  
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));  
continue;  
}  
 
// else it's a bit more complicated, there might  
// be a flat maxed out rectangle first, then a  
// triangular cutout or reverse; to find these  
// keep track of the current x values  
var x1old = x1, x2old = x2;  
 
// clip the y values, without shortcutting, we  
// go through all cases in turn  
 
// clip with ymin  
if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {  
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;  
y1 = axisy.min;  
}  
else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {  
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;  
y2 = axisy.min;  
}  
 
// clip with ymax  
if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {  
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;  
y1 = axisy.max;  
}  
else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {  
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;  
y2 = axisy.max;  
}  
 
// if the x value was changed we got a rectangle  
// to fill  
if (x1 != x1old) {  
ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));  
// it goes to (x1, y1), but we fill that below  
}  
 
// fill triangular section, this sometimes result  
// in redundant points if (x1, y1) hasn't changed  
// from previous line to, but we just ignore that  
ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));  
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));  
 
// fill the other rectangle if it's there  
if (x2 != x2old) {  
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));  
ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));  
}  
}  
}  
 
ctx.save();  
ctx.translate(plotOffset.left, plotOffset.top);  
ctx.lineJoin = "round";  
 
var lw = series.lines.lineWidth,  
sw = series.shadowSize;  
// FIXME: consider another form of shadow when filling is turned on  
if (lw > 0 && sw > 0) {  
// draw shadow as a thick and thin line with transparency  
ctx.lineWidth = sw;  
ctx.strokeStyle = "rgba(0,0,0,0.1)";  
// position shadow at angle from the mid of line  
var angle = Math.PI/18;  
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);  
ctx.lineWidth = sw/2;  
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);  
}  
 
ctx.lineWidth = lw;  
ctx.strokeStyle = series.color;  
var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);  
if (fillStyle) {  
ctx.fillStyle = fillStyle;  
plotLineArea(series.datapoints, series.xaxis, series.yaxis);  
}  
 
if (lw > 0)  
plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);  
ctx.restore();  
}  
 
function drawSeriesPoints(series) {  
function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {  
var points = datapoints.points, ps = datapoints.pointsize;  
 
for (var i = 0; i < points.length; i += ps) {  
var x = points[i], y = points[i + 1];  
if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)  
continue;  
 
ctx.beginPath();  
x = axisx.p2c(x);  
y = axisy.p2c(y) + offset;  
if (symbol == "circle")  
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);  
else  
symbol(ctx, x, y, radius, shadow);  
ctx.closePath();  
 
if (fillStyle) {  
ctx.fillStyle = fillStyle;  
ctx.fill();  
}  
ctx.stroke();  
}  
}  
 
ctx.save();  
ctx.translate(plotOffset.left, plotOffset.top);  
 
var lw = series.points.lineWidth,  
sw = series.shadowSize,  
radius = series.points.radius,  
symbol = series.points.symbol;  
if (lw > 0 && sw > 0) {  
// draw shadow in two steps  
var w = sw / 2;  
ctx.lineWidth = w;  
ctx.strokeStyle = "rgba(0,0,0,0.1)";  
plotPoints(series.datapoints, radius, null, w + w/2, true,  
series.xaxis, series.yaxis, symbol);  
 
ctx.strokeStyle = "rgba(0,0,0,0.2)";  
plotPoints(series.datapoints, radius, null, w/2, true,  
series.xaxis, series.yaxis, symbol);  
}  
 
ctx.lineWidth = lw;  
ctx.strokeStyle = series.color;  
plotPoints(series.datapoints, radius,  
getFillStyle(series.points, series.color), 0, false,  
series.xaxis, series.yaxis, symbol);  
ctx.restore();  
}  
 
function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {  
var left, right, bottom, top,  
drawLeft, drawRight, drawTop, drawBottom,  
tmp;  
 
// in horizontal mode, we start the bar from the left  
// instead of from the bottom so it appears to be  
// horizontal rather than vertical  
if (horizontal) {  
drawBottom = drawRight = drawTop = true;  
drawLeft = false;  
left = b;  
right = x;  
top = y + barLeft;  
bottom = y + barRight;  
 
// account for negative bars  
if (right < left) {  
tmp = right;  
right = left;  
left = tmp;  
drawLeft = true;  
drawRight = false;  
}  
}  
else {  
drawLeft = drawRight = drawTop = true;  
drawBottom = false;  
left = x + barLeft;  
right = x + barRight;  
bottom = b;  
top = y;  
 
// account for negative bars  
if (top < bottom) {  
tmp = top;  
top = bottom;  
bottom = tmp;  
drawBottom = true;  
drawTop = false;  
}  
}  
 
// clip  
if (right < axisx.min || left > axisx.max ||  
top < axisy.min || bottom > axisy.max)  
return;  
 
if (left < axisx.min) {  
left = axisx.min;  
drawLeft = false;  
}  
 
if (right > axisx.max) {  
right = axisx.max;  
drawRight = false;  
}  
 
if (bottom < axisy.min) {  
bottom = axisy.min;  
drawBottom = false;  
}  
 
if (top > axisy.max) {  
top = axisy.max;  
drawTop = false;  
}  
 
left = axisx.p2c(left);  
bottom = axisy.p2c(bottom);  
right = axisx.p2c(right);  
top = axisy.p2c(top);  
 
// fill the bar  
if (fillStyleCallback) {  
c.beginPath();  
c.moveTo(left, bottom);  
c.lineTo(left, top);  
c.lineTo(right, top);  
c.lineTo(right, bottom);  
c.fillStyle = fillStyleCallback(bottom, top);  
c.fill();  
}  
 
// draw outline  
if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {  
c.beginPath();  
 
// FIXME: inline moveTo is buggy with excanvas  
c.moveTo(left, bottom + offset);  
if (drawLeft)  
c.lineTo(left, top + offset);  
else  
c.moveTo(left, top + offset);  
if (drawTop)  
c.lineTo(right, top + offset);  
else  
c.moveTo(right, top + offset);  
if (drawRight)  
c.lineTo(right, bottom + offset);  
else  
c.moveTo(right, bottom + offset);  
if (drawBottom)  
c.lineTo(left, bottom + offset);  
else  
c.moveTo(left, bottom + offset);  
c.stroke();  
}  
}  
 
function drawSeriesBars(series) {  
function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {  
var points = datapoints.points, ps = datapoints.pointsize;  
 
for (var i = 0; i < points.length; i += ps) {  
if (points[i] == null)  
continue;  
drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);  
}  
}  
 
ctx.save();  
ctx.translate(plotOffset.left, plotOffset.top);  
 
// FIXME: figure out a way to add shadows (for instance along the right edge)  
ctx.lineWidth = series.bars.lineWidth;  
ctx.strokeStyle = series.color;  
var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;  
var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;  
plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);  
ctx.restore();  
}  
 
function getFillStyle(filloptions, seriesColor, bottom, top) {  
var fill = filloptions.fill;  
if (!fill)  
return null;  
 
if (filloptions.fillColor)  
return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);  
 
var c = $.color.parse(seriesColor);  
c.a = typeof fill == "number" ? fill : 0.4;  
c.normalize();  
return c.toString();  
}  
 
function insertLegend() {  
placeholder.find(".legend").remove();  
 
if (!options.legend.show)  
return;  
 
var fragments = [], rowStarted = false,  
lf = options.legend.labelFormatter, s, label;  
for (var i = 0; i < series.length; ++i) {  
s = series[i];  
label = s.label;  
if (!label)  
continue;  
 
if (i % options.legend.noColumns == 0) {  
if (rowStarted)  
fragments.push('</tr>');  
fragments.push('<tr>');  
rowStarted = true;  
}  
 
if (lf)  
label = lf(label, s);  
 
fragments.push(  
'<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +  
'<td class="legendLabel">' + label + '</td>');  
}  
if (rowStarted)  
fragments.push('</tr>');  
 
if (fragments.length == 0)  
return;  
 
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';  
if (options.legend.container != null)  
$(options.legend.container).html(table);  
else {  
var pos = "",  
p = options.legend.position,  
m = options.legend.margin;  
if (m[0] == null)  
m = [m, m];  
if (p.charAt(0) == "n")  
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';  
else if (p.charAt(0) == "s")  
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';  
if (p.charAt(1) == "e")  
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';  
else if (p.charAt(1) == "w")  
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';  
var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);  
if (options.legend.backgroundOpacity != 0.0) {  
// put in the transparent background  
// separately to avoid blended labels and  
// label boxes  
var c = options.legend.backgroundColor;  
if (c == null) {  
c = options.grid.backgroundColor;  
if (c && typeof c == "string")  
c = $.color.parse(c);  
else  
c = $.color.extract(legend, 'background-color');  
c.a = 1;  
c = c.toString();  
}  
var div = legend.children();  
$('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);  
}  
}  
}  
 
 
// interactive features  
 
var highlights = [],  
redrawTimeout = null;  
 
// returns the data item the mouse is over, or null if none is found  
function findNearbyItem(mouseX, mouseY, seriesFilter) {  
var maxDistance = options.grid.mouseActiveRadius,  
smallestDistance = maxDistance * maxDistance + 1,  
item = null, foundPoint = false, i, j;  
 
for (i = series.length - 1; i >= 0; --i) {  
if (!seriesFilter(series[i]))  
continue;  
 
var s = series[i],  
axisx = s.xaxis,  
axisy = s.yaxis,  
points = s.datapoints.points,  
ps = s.datapoints.pointsize,  
mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster  
my = axisy.c2p(mouseY),  
maxx = maxDistance / axisx.scale,  
maxy = maxDistance / axisy.scale;  
 
// with inverse transforms, we can't use the maxx/maxy  
// optimization, sadly  
if (axisx.options.inverseTransform)  
maxx = Number.MAX_VALUE;  
if (axisy.options.inverseTransform)  
maxy = Number.MAX_VALUE;  
 
if (s.lines.show || s.points.show) {  
for (j = 0; j < points.length; j += ps) {  
var x = points[j], y = points[j + 1];  
if (x == null)  
continue;  
 
// For points and lines, the cursor must be within a  
// certain distance to the data point  
if (x - mx > maxx || x - mx < -maxx ||  
y - my > maxy || y - my < -maxy)  
continue;  
 
// We have to calculate distances in pixels, not in  
// data units, because the scales of the axes may be different  
var dx = Math.abs(axisx.p2c(x) - mouseX),  
dy = Math.abs(axisy.p2c(y) - mouseY),  
dist = dx * dx + dy * dy; // we save the sqrt  
 
// use <= to ensure last point takes precedence  
// (last generally means on top of)  
if (dist < smallestDistance) {  
smallestDistance = dist;  
item = [i, j / ps];  
}  
}  
}  
 
if (s.bars.show && !item) { // no other point can be nearby  
var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,  
barRight = barLeft + s.bars.barWidth;  
 
for (j = 0; j < points.length; j += ps) {  
var x = points[j], y = points[j + 1], b = points[j + 2];  
if (x == null)  
continue;  
 
// for a bar graph, the cursor must be inside the bar  
if (series[i].bars.horizontal ?  
(mx <= Math.max(b, x) && mx >= Math.min(b, x) &&  
my >= y + barLeft && my <= y + barRight) :  
(mx >= x + barLeft && mx <= x + barRight &&  
my >= Math.min(b, y) && my <= Math.max(b, y)))  
item = [i, j / ps];  
}  
}  
}  
 
if (item) {  
i = item[0];  
j = item[1];  
ps = series[i].datapoints.pointsize;  
 
return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),  
dataIndex: j,  
series: series[i],  
seriesIndex: i };  
}  
 
return null;  
}  
 
function onMouseMove(e) {  
if (options.grid.hoverable)  
triggerClickHoverEvent("plothover", e,  
function (s) { return s["hoverable"] != false; });  
}  
 
function onMouseLeave(e) {  
if (options.grid.hoverable)  
triggerClickHoverEvent("plothover", e,  
function (s) { return false; });  
}  
 
function onClick(e) {  
triggerClickHoverEvent("plotclick", e,  
function (s) { return s["clickable"] != false; });  
}  
 
// trigger click or hover event (they send the same parameters  
// so we share their code)  
function triggerClickHoverEvent(eventname, event, seriesFilter) {  
var offset = eventHolder.offset(),  
canvasX = event.pageX - offset.left - plotOffset.left,  
canvasY = event.pageY - offset.top - plotOffset.top,  
pos = canvasToAxisCoords({ left: canvasX, top: canvasY });  
 
pos.pageX = event.pageX;  
pos.pageY = event.pageY;  
 
var item = findNearbyItem(canvasX, canvasY, seriesFilter);  
 
if (item) {  
// fill in mouse pos for any listeners out there  
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);  
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);  
}  
 
if (options.grid.autoHighlight) {  
// clear auto-highlights  
for (var i = 0; i < highlights.length; ++i) {  
var h = highlights[i];  
if (h.auto == eventname &&  
!(item && h.series == item.series &&  
h.point[0] == item.datapoint[0] &&  
h.point[1] == item.datapoint[1]))  
unhighlight(h.series, h.point);  
}  
 
if (item)  
highlight(item.series, item.datapoint, eventname);  
}  
 
placeholder.trigger(eventname, [ pos, item ]);  
}  
 
function triggerRedrawOverlay() {  
if (!redrawTimeout)  
redrawTimeout = setTimeout(drawOverlay, 30);  
}  
 
function drawOverlay() {  
redrawTimeout = null;  
 
// draw highlights  
octx.save();  
octx.clearRect(0, 0, canvasWidth, canvasHeight);  
octx.translate(plotOffset.left, plotOffset.top);  
 
var i, hi;  
for (i = 0; i < highlights.length; ++i) {  
hi = highlights[i];  
 
if (hi.series.bars.show)  
drawBarHighlight(hi.series, hi.point);  
else  
drawPointHighlight(hi.series, hi.point);  
}  
octx.restore();  
 
executeHooks(hooks.drawOverlay, [octx]);  
}  
 
function highlight(s, point, auto) {  
if (typeof s == "number")  
s = series[s];  
 
if (typeof point == "number") {  
var ps = s.datapoints.pointsize;  
point = s.datapoints.points.slice(ps * point, ps * (point + 1));  
}  
 
var i = indexOfHighlight(s, point);  
if (i == -1) {  
highlights.push({ series: s, point: point, auto: auto });  
 
triggerRedrawOverlay();  
}  
else if (!auto)  
highlights[i].auto = false;  
}  
 
function unhighlight(s, point) {  
if (s == null && point == null) {  
highlights = [];  
triggerRedrawOverlay();  
}  
 
if (typeof s == "number")  
s = series[s];  
 
if (typeof point == "number")  
point = s.data[point];  
 
var i = indexOfHighlight(s, point);  
if (i != -1) {  
highlights.splice(i, 1);  
 
triggerRedrawOverlay();  
}  
}  
 
function indexOfHighlight(s, p) {  
for (var i = 0; i < highlights.length; ++i) {  
var h = highlights[i];  
if (h.series == s && h.point[0] == p[0]  
&& h.point[1] == p[1])  
return i;  
}  
return -1;  
}  
 
function drawPointHighlight(series, point) {  
var x = point[0], y = point[1],  
axisx = series.xaxis, axisy = series.yaxis;  
 
if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)  
return;  
 
var pointRadius = series.points.radius + series.points.lineWidth / 2;  
octx.lineWidth = pointRadius;  
octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();  
var radius = 1.5 * pointRadius,  
x = axisx.p2c(x),  
y = axisy.p2c(y);  
 
octx.beginPath();  
if (series.points.symbol == "circle")  
octx.arc(x, y, radius, 0, 2 * Math.PI, false);  
else  
series.points.symbol(octx, x, y, radius, false);  
octx.closePath();  
octx.stroke();  
}  
 
function drawBarHighlight(series, point) {  
octx.lineWidth = series.bars.lineWidth;  
octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();  
var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();  
var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;  
drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,  
0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);  
}  
 
function getColorOrGradient(spec, bottom, top, defaultColor) {  
if (typeof spec == "string")  
return spec;  
else {  
// assume this is a gradient spec; IE currently only  
// supports a simple vertical gradient properly, so that's  
// what we support too  
var gradient = ctx.createLinearGradient(0, top, 0, bottom);  
 
for (var i = 0, l = spec.colors.length; i < l; ++i) {  
var c = spec.colors[i];  
if (typeof c != "string") {  
var co = $.color.parse(defaultColor);  
if (c.brightness != null)  
co = co.scale('rgb', c.brightness)  
if (c.opacity != null)  
co.a *= c.opacity;  
c = co.toString();  
}  
gradient.addColorStop(i / (l - 1), c);  
}  
 
return gradient;  
}  
}  
}  
 
$.plot = function(placeholder, data, options) {  
//var t0 = new Date();  
var plot = new Plot($(placeholder), data, options, $.plot.plugins);  
//(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));  
return plot;  
};  
 
$.plot.version = "0.7";  
 
$.plot.plugins = [];  
 
// returns a string with the date d formatted according to fmt  
$.plot.formatDate = function(d, fmt, monthNames) {  
var leftPad = function(n) {  
n = "" + n;  
return n.length == 1 ? "0" + n : n;  
};  
 
var r = [];  
var escape = false, padNext = false;  
var hours = d.getUTCHours();  
var isAM = hours < 12;  
if (monthNames == null)  
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];  
 
if (fmt.search(/%p|%P/) != -1) {  
if (hours > 12) {  
hours = hours - 12;  
} else if (hours == 0) {  
hours = 12;  
}  
}  
for (var i = 0; i < fmt.length; ++i) {  
var c = fmt.charAt(i);  
 
if (escape) {  
switch (c) {  
case 'h': c = "" + hours; break;  
case 'H': c = leftPad(hours); break;  
case 'M': c = leftPad(d.getUTCMinutes()); break;  
case 'S': c = leftPad(d.getUTCSeconds()); break;  
case 'd': c = "" + d.getUTCDate(); break;  
case 'm': c = "" + (d.getUTCMonth() + 1); break;  
case 'y': c = "" + d.getUTCFullYear(); break;  
case 'b': c = "" + monthNames[d.getUTCMonth()]; break;  
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;  
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;  
case '0': c = ""; padNext = true; break;  
}  
if (c && padNext) {  
c = leftPad(c);  
padNext = false;  
}  
r.push(c);  
if (!padNext)  
escape = false;  
}  
else {  
if (c == "%")  
escape = true;  
else  
r.push(c);  
}  
}  
return r.join("");  
};  
 
// round to nearby lower multiple of base  
function floorInBase(n, base) {  
return base * Math.floor(n / base);  
}  
 
})(jQuery);  
 
import datetime  
 
from pylons import config  
from sqlalchemy import Table, select, func, and_  
 
import ckan.plugins as p  
import ckan.model as model  
 
cache_enabled = p.toolkit.asbool(config.get('ckanext.stats.cache_enabled', 'True'))  
 
if cache_enabled:  
from pylons import cache  
our_cache = cache.get_cache('stats', type='dbm')  
 
DATE_FORMAT = '%Y-%m-%d'  
 
def table(name):  
return Table(name, model.meta.metadata, autoload=True)  
 
def datetime2date(datetime_):  
return datetime.date(datetime_.year, datetime_.month, datetime_.day)  
 
 
class Stats(object):  
@classmethod  
def top_rated_packages(cls, limit=10):  
# NB Not using sqlalchemy as sqla 0.4 doesn't work using both group_by  
# and apply_avg  
package = table('package')  
rating = table('rating')  
sql = select([package.c.id, func.avg(rating.c.rating), func.count(rating.c.rating)], from_obj=[package.join(rating)]).\  
group_by(package.c.id).\  
order_by(func.avg(rating.c.rating).desc(), func.count(rating.c.rating).desc()).\  
limit(limit)  
res_ids = model.Session.execute(sql).fetchall()  
res_pkgs = [(model.Session.query(model.Package).get(unicode(pkg_id)), avg, num) for pkg_id, avg, num in res_ids]  
return res_pkgs  
 
@classmethod  
def most_edited_packages(cls, limit=10):  
package_revision = table('package_revision')  
s = select([package_revision.c.id, func.count(package_revision.c.revision_id)]).\  
group_by(package_revision.c.id).\  
order_by(func.count(package_revision.c.revision_id).desc()).\  
limit(limit)  
res_ids = model.Session.execute(s).fetchall()  
res_pkgs = [(model.Session.query(model.Package).get(unicode(pkg_id)), val) for pkg_id, val in res_ids]  
return res_pkgs  
 
@classmethod  
def largest_groups(cls, limit=10):  
member = table('member')  
s = select([member.c.group_id, func.count(member.c.table_id)]).\  
group_by(member.c.group_id).\  
where(and_(member.c.group_id!=None, member.c.table_name=='package')).\  
order_by(func.count(member.c.table_id).desc()).\  
limit(limit)  
 
res_ids = model.Session.execute(s).fetchall()  
res_groups = [(model.Session.query(model.Group).get(unicode(group_id)), val) for group_id, val in res_ids]  
return res_groups  
 
@classmethod  
def top_tags(cls, limit=10, returned_tag_info='object'): # by package  
assert returned_tag_info in ('name', 'id', 'object')  
tag = table('tag')  
package_tag = table('package_tag')  
#TODO filter out tags with state=deleted  
if returned_tag_info == 'name':  
from_obj = [package_tag.join(tag)]  
tag_column = tag.c.name  
else:  
from_obj = None  
tag_column = package_tag.c.tag_id  
s = select([tag_column, func.count(package_tag.c.package_id)],  
from_obj=from_obj)  
s = s.group_by(tag_column).\  
order_by(func.count(package_tag.c.package_id).desc()).\  
limit(limit)  
res_col = model.Session.execute(s).fetchall()  
if returned_tag_info in ('id', 'name'):  
return res_col  
elif returned_tag_info == 'object':  
res_tags = [(model.Session.query(model.Tag).get(unicode(tag_id)), val) for tag_id, val in res_col]  
return res_tags  
 
@classmethod  
def top_package_owners(cls, limit=10):  
package_role = table('package_role')  
user_object_role = table('user_object_role')  
s = select([user_object_role.c.user_id, func.count(user_object_role.c.role)], from_obj=[user_object_role.join(package_role)]).\  
where(user_object_role.c.role==model.authz.Role.ADMIN).\  
where(user_object_role.c.user_id!=None).\  
group_by(user_object_role.c.user_id).\  
order_by(func.count(user_object_role.c.role).desc()).\  
limit(limit)  
res_ids = model.Session.execute(s).fetchall()  
res_users = [(model.Session.query(model.User).get(unicode(user_id)), val) for user_id, val in res_ids]  
return res_users  
 
class RevisionStats(object):  
@classmethod  
def package_addition_rate(cls, weeks_ago=0):  
week_commenced = cls.get_date_weeks_ago(weeks_ago)  
return cls.get_objects_in_a_week(week_commenced,  
type_='package_addition_rate')  
 
@classmethod  
def package_revision_rate(cls, weeks_ago=0):  
week_commenced = cls.get_date_weeks_ago(weeks_ago)  
return cls.get_objects_in_a_week(week_commenced,  
type_='package_revision_rate')  
 
@classmethod  
def get_date_weeks_ago(cls, weeks_ago):  
'''  
@param weeks_ago: specify how many weeks ago to give count for  
(0 = this week so far)  
'''  
date_ = datetime.date.today()  
return date_ - datetime.timedelta(days=  
datetime.date.weekday(date_) + 7 * weeks_ago)  
 
@classmethod  
def get_week_dates(cls, weeks_ago):  
'''  
@param weeks_ago: specify how many weeks ago to give count for  
(0 = this week so far)  
'''  
package_revision = table('package_revision')  
revision = table('revision')  
today = datetime.date.today()  
date_from = datetime.datetime(today.year, today.month, today.day) -\  
datetime.timedelta(days=datetime.date.weekday(today) + \  
7 * weeks_ago)  
date_to = date_from + datetime.timedelta(days=7)  
return (date_from, date_to)  
 
@classmethod  
def get_date_week_started(cls, date_):  
assert isinstance(date_, datetime.date)  
if isinstance(date_, datetime.datetime):  
date_ = datetime2date(date_)  
return date_ - datetime.timedelta(days=datetime.date.weekday(date_))  
 
@classmethod  
def get_package_revisions(cls):  
'''  
@return: Returns list of revisions and date of them, in  
format: [(id, date), ...]  
'''  
package_revision = table('package_revision')  
revision = table('revision')  
s = select([package_revision.c.id, revision.c.timestamp], from_obj=[package_revision.join(revision)]).order_by(revision.c.timestamp)  
res = model.Session.execute(s).fetchall() # [(id, datetime), ...]  
return res  
 
@classmethod  
def get_new_packages(cls):  
'''  
@return: Returns list of new pkgs and date when they were created, in  
format: [(id, date_ordinal), ...]  
'''  
def new_packages():  
# Can't filter by time in select because 'min' function has to  
# be 'for all time' else you get first revision in the time period.  
package_revision = table('package_revision')  
revision = table('revision')  
s = select([package_revision.c.id, func.min(revision.c.timestamp)], from_obj=[package_revision.join(revision)]).group_by(package_revision.c.id).order_by(func.min(revision.c.timestamp))  
res = model.Session.execute(s).fetchall() # [(id, datetime), ...]  
res_pickleable = []  
for pkg_id, created_datetime in res:  
res_pickleable.append((pkg_id, created_datetime.toordinal()))  
return res_pickleable  
if cache_enabled:  
week_commences = cls.get_date_week_started(datetime.date.today())  
key = 'all_new_packages_%s' + week_commences.strftime(DATE_FORMAT)  
new_packages = our_cache.get_value(key=key,  
createfunc=new_packages)  
else:  
new_packages = new_packages()  
return new_packages  
 
@classmethod  
def get_deleted_packages(cls):  
'''  
@return: Returns list of deleted pkgs and date when they were deleted, in  
format: [(id, date_ordinal), ...]  
'''  
def deleted_packages():  
# Can't filter by time in select because 'min' function has to  
# be 'for all time' else you get first revision in the time period.  
package_revision = table('package_revision')  
revision = table('revision')  
s = select([package_revision.c.id, func.min(revision.c.timestamp)], from_obj=[package_revision.join(revision)]).\  
where(package_revision.c.state==model.State.DELETED).\  
group_by(package_revision.c.id).\  
order_by(func.min(revision.c.timestamp))  
res = model.Session.execute(s).fetchall() # [(id, datetime), ...]  
res_pickleable = []  
for pkg_id, deleted_datetime in res:  
res_pickleable.append((pkg_id, deleted_datetime.toordinal()))  
return res_pickleable  
if cache_enabled:  
week_commences = cls.get_date_week_started(datetime.date.today())  
key = 'all_deleted_packages_%s' + week_commences.strftime(DATE_FORMAT)  
deleted_packages = our_cache.get_value(key=key,  
createfunc=deleted_packages)  
else:  
deleted_packages = deleted_packages()  
return deleted_packages  
 
@classmethod  
def get_num_packages_by_week(cls):  
def num_packages():  
new_packages_by_week = cls.get_by_week('new_packages')  
deleted_packages_by_week = cls.get_by_week('deleted_packages')  
first_date = (min(datetime.datetime.strptime(new_packages_by_week[0][0], DATE_FORMAT),  
datetime.datetime.strptime(deleted_packages_by_week[0][0], DATE_FORMAT))).date()  
cls._cumulative_num_pkgs = 0  
new_pkgs = []  
deleted_pkgs = []  
def build_weekly_stats(week_commences, new_pkg_ids, deleted_pkg_ids):  
num_pkgs = len(new_pkg_ids) - len(deleted_pkg_ids)  
new_pkgs.extend([model.Session.query(model.Package).get(id).name for id in new_pkg_ids])  
deleted_pkgs.extend([model.Session.query(model.Package).get(id).name for id in deleted_pkg_ids])  
cls._cumulative_num_pkgs += num_pkgs  
return (week_commences.strftime(DATE_FORMAT),  
num_pkgs, cls._cumulative_num_pkgs)  
week_ends = first_date  
today = datetime.date.today()  
new_package_week_index = 0  
deleted_package_week_index = 0  
weekly_numbers = [] # [(week_commences, num_packages, cumulative_num_pkgs])]  
while week_ends <= today:  
week_commences = week_ends  
week_ends = week_commences + datetime.timedelta(days=7)  
if datetime.datetime.strptime(new_packages_by_week[new_package_week_index][0], DATE_FORMAT).date() == week_commences:  
new_pkg_ids = new_packages_by_week[new_package_week_index][1]  
new_package_week_index += 1  
else:  
new_pkg_ids = []  
if datetime.datetime.strptime(deleted_packages_by_week[deleted_package_week_index][0], DATE_FORMAT).date() == week_commences:  
deleted_pkg_ids = deleted_packages_by_week[deleted_package_week_index][1]  
deleted_package_week_index += 1  
else:  
deleted_pkg_ids = []  
weekly_numbers.append(build_weekly_stats(week_commences, new_pkg_ids, deleted_pkg_ids))  
# just check we got to the end of each count  
assert new_package_week_index == len(new_packages_by_week)  
assert deleted_package_week_index == len(deleted_packages_by_week)  
return weekly_numbers  
if cache_enabled:  
week_commences = cls.get_date_week_started(datetime.date.today())  
key = 'number_packages_%s' + week_commences.strftime(DATE_FORMAT)  
num_packages = our_cache.get_value(key=key,  
createfunc=num_packages)  
else:  
num_packages = num_packages()  
return num_packages  
 
@classmethod  
def get_by_week(cls, object_type):  
cls._object_type = object_type  
def objects_by_week():  
if cls._object_type == 'new_packages':  
objects = cls.get_new_packages()  
def get_date(object_date):  
return datetime.date.fromordinal(object_date)  
elif cls._object_type == 'deleted_packages':  
objects = cls.get_deleted_packages()  
def get_date(object_date):  
return datetime.date.fromordinal(object_date)  
elif cls._object_type == 'package_revisions':  
objects = cls.get_package_revisions()  
def get_date(object_date):  
return datetime2date(object_date)  
else:  
raise NotImplementedError()  
first_date = get_date(objects[0][1]) if objects else datetime.date.today()  
week_commences = cls.get_date_week_started(first_date)  
week_ends = week_commences + datetime.timedelta(days=7)  
week_index = 0  
weekly_pkg_ids = [] # [(week_commences, [pkg_id1, pkg_id2, ...])]  
pkg_id_stack = []  
cls._cumulative_num_pkgs = 0  
def build_weekly_stats(week_commences, pkg_ids):  
num_pkgs = len(pkg_ids)  
cls._cumulative_num_pkgs += num_pkgs  
return (week_commences.strftime(DATE_FORMAT),  
pkg_ids, num_pkgs, cls._cumulative_num_pkgs)  
for pkg_id, date_field in objects:  
date_ = get_date(date_field)  
if date_ >= week_ends:  
weekly_pkg_ids.append(build_weekly_stats(week_commences, pkg_id_stack))  
pkg_id_stack = []  
week_commences = week_ends  
week_ends = week_commences + datetime.timedelta(days=7)  
pkg_id_stack.append(pkg_id)  
weekly_pkg_ids.append(build_weekly_stats(week_commences, pkg_id_stack))  
today = datetime.date.today()  
while week_ends <= today:  
week_commences = week_ends  
week_ends = week_commences + datetime.timedelta(days=7)  
weekly_pkg_ids.append(build_weekly_stats(week_commences, []))  
return weekly_pkg_ids  
if cache_enabled:  
week_commences = cls.get_date_week_started(datetime.date.today())  
key = '%s_by_week_%s' % (cls._object_type, week_commences.strftime(DATE_FORMAT))  
objects_by_week_ = our_cache.get_value(key=key,  
createfunc=objects_by_week)  
else:  
objects_by_week_ = objects_by_week()  
return objects_by_week_  
 
@classmethod  
def get_objects_in_a_week(cls, date_week_commences,  
type_='new-package-rate'):  
'''  
@param type: Specifies what to return about the specified week:  
"package_addition_rate" number of new packages  
"package_revision_rate" number of package revisions  
"new_packages" a list of the packages created  
in a tuple with the date.  
"deleted_packages" a list of the packages deleted  
in a tuple with the date.  
@param dates: date range of interest - a tuple:  
(start_date, end_date)  
'''  
assert isinstance(date_week_commences, datetime.date)  
if type_ in ('package_addition_rate', 'new_packages'):  
object_type = 'new_packages'  
elif type_ == 'deleted_packages':  
object_type = 'deleted_packages'  
elif type_ == 'package_revision_rate':  
object_type = 'package_revisions'  
else:  
raise NotImplementedError()  
objects_by_week = cls.get_by_week(object_type)  
date_wc_str = date_week_commences.strftime(DATE_FORMAT)  
object_ids = None  
for objects_in_a_week in objects_by_week:  
if objects_in_a_week[0] == date_wc_str:  
object_ids = objects_in_a_week[1]  
break  
if object_ids is None:  
raise TypeError('Week specified is outside range')  
assert isinstance(object_ids, list)  
if type_ in ('package_revision_rate', 'package_addition_rate'):  
return len(object_ids)  
elif type_ in ('new_packages', 'deleted_packages'):  
return [ model.Session.query(model.Package).get(pkg_id) \  
for pkg_id in object_ids ]  
 
{% extends "page.html" %}  
 
{% block breadcrumb_content %}  
<li class="active">{{ 'Statistics' }}</li>  
{% endblock %}  
 
{% block primary_content %}  
<article class="module">  
<section id="stats-total-datasets" class="module-content tab-content active">  
<h2>{{ _('Total number of Datasets') }}</h2>  
 
{% set xaxis = {'mode': 'time', 'timeformat': '%y-%b'} %}  
{% set yaxis = {'min': 0} %}  
<table class="table table-chunky table-bordered table-striped" data-module="plot" data-module-xaxis="{{ h.dump_json(xaxis) }}" data-module-yaxis="{{ h.dump_json(yaxis) }}">  
<thead>  
<tr>  
<th>{{ _("Date") }}</th>  
<th>{{ _("Total datasets") }}</th>  
</tr>  
</thead>  
<tbody>  
{% for row in c.raw_packages_by_week %}  
<tr>  
<th data-type="date" data-value="{{ row.date.strftime("%s") }}"><time datetime="{{ row.date.isoformat() }}">{{ h.render_datetime(row.date) }}</time></th>  
<td>{{ row.total_packages }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
</section>  
 
<section id="stats-dataset-revisions" class="module-content tab-content">  
<h2>{{ _('Dataset Revisions per Week') }}</h2>  
 
{% set xaxis = {'mode': 'time', 'timeformat': '%y-%b'} %}  
{% set lines = {'fill': 1} %}  
<table class="table table-chunky table-bordered table-striped" data-module="plot" data-module-xaxis="{{ h.dump_json(xaxis) }}" data-module-lines="{{ h.dump_json(lines) }}">  
<thead>  
<tr>  
<th>{{ _("Date") }}</th>  
<th>{{ _("All dataset revisions") }}</th>  
<th>{{ _("New datasets") }}</th>  
</tr>  
</thead>  
<tbody>  
{% for row in c.raw_all_package_revisions %}  
<tr>  
<th data-type="date" data-value="{{ row.date.strftime("%s") }}"><time datetime="{{ row.date.isoformat() }}">{{ h.render_datetime(row.date) }}</time></th>  
<td>{{ row.total_revisions }}</td>  
<td>{{ c.raw_new_datasets[loop.index0].new_packages }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
</section>  
 
<section id="stats-top-rated" class="module-content tab-content">  
<h2>{{ _('Top Rated Datasets') }}</h2>  
{% if c.top_rated_packages %}  
<table class="table table-chunky table-bordered table-striped">  
<thead>  
<tr>  
<th>Dataset</th>  
<th class="metric">{{ _('Average rating') }}</th>  
<th class="metric">{{ _('Number of ratings') }}</th>  
</tr>  
</thead>  
<tbody>  
{% for package, rating, num_ratings in c.top_rated_packages %}  
<tr>  
<th>{{ h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name)) }}</th>  
<td class="metric">{{ rating }}</td>  
<td class="metric">{{ num_ratings }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
{% else %}  
<p class="empty">{{ _('No ratings') }}</p>  
{% endif %}  
</section>  
 
<section id="stats-most-edited" class="module-content tab-content">  
<h2>{{ _('Most Edited Datasets') }}</h2>  
{% if c.most_edited_packages %}  
<table class="table table-chunky table-bordered table-striped">  
<thead>  
<tr>  
<th>{{ _('Dataset') }}</th>  
<th class="metric">{{ _('Number of edits') }}</th>  
</tr>  
</thead>  
<tbody>  
{% for package, edits in c.most_edited_packages %}  
<tr py:for="package, edits in c.most_edited_packages">  
<td>{{ h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name)) }}</td>  
<td class="metric">{{ edits }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
{% else %}  
<p class="empty">{{ _('No edited datasets') }}</p>  
{% endif %}  
</section>  
 
<section id="stats-largest-groups" class="module-content tab-content">  
<h2>{{ _('Largest Groups') }}</h2>  
{% if c.largest_groups %}  
<table class="table table-chunky table-bordered table-striped">  
<thead>  
<tr>  
<th>{{ _('Group') }}</th>  
<th class="metric">{{ _('Number of datasets') }}</th>  
</tr>  
</thead>  
<tbody>  
{% for group, num_packages in c.largest_groups %}  
<tr>  
<td>{{ h.link_to(group.title or group.name, h.url_for(controller='group', action='read', id=group.name)) }}</td>  
<td class="metric">{{ num_packages }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
{% else %}  
<p class="empty">{{ _('No groups') }}</p>  
{% endif %}  
</section>  
 
<section id="stats-top-tags" class="module-content tab-content">  
<h2>{{ _('Top Tags') }}</h2>  
<table class="table table-chunky table-bordered table-striped">  
<thead>  
<tr>  
<th>{{ _('Tag Name') }}</th>  
<th class="metric">{{ _('Number of Datasets') }}</th>  
</tr>  
</thead>  
<tbody>  
{% for tag, num_packages in c.top_tags %}  
<tr>  
<td>{{ h.link_to(tag.name, h.url_for(controller='package', action='search', tags=tag.name)) }}</td>  
<td class="metric">{{ num_packages }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
</section>  
 
<section id="stats-most-owned" class="module-content tab-content">  
<h2>{{ _('Users Owning Most Datasets') }}</h2>  
<table class="table table-chunky table-bordered table-striped">  
<thead>  
<tr>  
<th>{{ _('User') }}</th>  
<th class="metric">{{ _('Number of Datasets') }}</th>  
</tr>  
</thead>  
<tbody>  
{% for user, num_packages in c.top_package_owners %}  
<tr>  
<td class="media">{{ h.linked_user(user) }}</td>  
<td class="metric">{{ num_packages }}</td>  
</tr>  
{% endfor %}  
</tbody>  
</table>  
</section>  
</article>  
{% endblock %}  
 
{% block secondary_content %}  
<section class="module module-narrow">  
<h2 class="module-heading"><i class="icon-bar-chart icon-medium"></i> {{ _('Statistics Menu') }}</h2>  
<nav data-module="stats-nav">  
<ul class="unstyled nav nav-simple">  
<li class="nav-item active"><a href="#stats-total-datasets" data-toggle="tab">{{ _('Total Number of Datasets') }}</a></li>  
<li class="nav-item"><a href="#stats-dataset-revisions" data-toggle="tab">{{ _('Dataset Revisions per Week') }}</a></li>  
<li class="nav-item"><a href="#stats-top-rated" data-toggle="tab">{{ _('Top Rated Datasets') }}</a></li>  
<li class="nav-item"><a href="#stats-most-edited" data-toggle="tab">{{ _('Most Edited Datasets') }}</a></li>  
<li class="nav-item"><a href="#stats-largest-groups" data-toggle="tab">{{ _('Largest Groups') }}</a></li>  
<li class="nav-item"><a href="#stats-top-tags" data-toggle="tab">{{ _('Top Tags') }}</a></li>  
<li class="nav-item"><a href="#stats-most-owned" data-toggle="tab">{{ _('Users Owning Most Datasets') }}</a></li>  
</ul>  
</nav>  
</section>  
{% endblock %}  
 
{% block scripts %}  
{{ super() }}  
{#  
Hellish hack to get excanvas to work in IE8. We disable html5shiv from  
overriding the createElement() method on this page.  
See: http://stackoverflow.com/questions/10208062/using-flot-with-bootstrap-ie8-incompatibility  
#}  
{% resource "vendor/block_html5_shim" %}  
{% resource "ckanext_stats/stats" %}  
{% endblock %}  
 
<html xmlns:py="http://genshi.edgewall.org/"  
xmlns:i18n="http://genshi.edgewall.org/i18n"  
xmlns:xi="http://www.w3.org/2001/XInclude"  
py:strip="">  
 
<py:def i18n:msg="" function="page_title">Statistics</py:def>  
 
<py:def function="page_heading">  
Statistics  
</py:def>  
 
<py:def function="optional_feed">  
<!--!  
Hellish hack to get excanvas to work in IE8. We disable html5shiv from  
overriding the createElement() method on this page.  
See: http://stackoverflow.com/questions/10208062/using-flot-with-bootstrap-ie8-incompatibility  
-->  
<!--[if lte IE 8 ]><script>var html5 = {shivMethods: false};</script><![endif]-->  
</py:def>  
 
<py:def function="optional_head">  
<style type="text/css">  
body #sidebar {  
display: none;  
}  
 
body #content {  
width: 950px;  
}  
 
h3 {  
margin-top: 20px;  
}  
 
.graph {  
width: 950px;  
height: 300px;  
margin-bottom: 20px;  
}  
 
.metric {  
width: 30%;  
}  
 
</style>  
</py:def>  
 
<py:match path="minornavigation">  
<ul class="tabbed">  
<li class="current-tab">  
<a href="">Home</a>  
</li>  
</ul>  
</py:match>  
 
<div py:match="content">  
<h3>Total number of Datasets</h3>  
<div id="new_packages_graph" class="graph"></div>  
 
<h3>Revisions to Datasets per week</h3>  
<div id="package_revisions_graph" class="graph"></div>  
 
<h3>Top Rated Datasets</h3>  
<table py:if="c.top_rated_packages" class="table table-bordered table-striped">  
<tr><th>Dataset</th><th>Average rating</th><th class="metric">Number of ratings</th></tr>  
<tr py:for="package, rating, num_ratings in c.top_rated_packages">  
<td>${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}</td><td>${rating}</td><td>${num_ratings}</td>  
</tr>  
</table>  
<p py:if="not c.top_rated_packages">No ratings</p>  
 
<h3>Most Edited Datasets</h3>  
<table class="table table-bordered table-striped">  
<tr><th>Dataset</th><th class="metric">Number of edits</th></tr>  
<tr py:for="package, edits in c.most_edited_packages">  
<td>${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}</td><td>${edits}</td>  
</tr>  
</table>  
 
<h3>Largest Groups</h3>  
<table class="table table-bordered table-striped">  
<tr><th>Group</th><th class="metric">Number of datasets</th></tr>  
<tr py:for="group, num_packages in c.largest_groups">  
<td>${h.link_to(group.title or group.name, h.url_for(controller='group', action='read', id=group.name))}</td><td>${num_packages}</td>  
</tr>  
</table>  
 
<h3>Top Tags</h3>  
<table class="table table-bordered table-striped">  
<tr py:for="tag, num_packages in c.top_tags">  
<td>${h.link_to(tag.name, h.url_for(controller='tag', action='read', id=tag.name))}</td><td class="metric">${num_packages}</td>  
</tr>  
</table>  
 
<h3>Users owning most datasets</h3>  
<table class="table table-bordered table-striped">  
<tr py:for="user, num_packages in c.top_package_owners">  
<td>${h.linked_user(user)}</td><td class="metric">${num_packages}</td>  
</tr>  
</table>  
 
<p>  
Page last updated:  
<?python  
import datetime  
?>  
${datetime.datetime.now().strftime('%c')}  
</p>  
</div>  
 
<py:def function="optional_footer">  
<script type="text/javascript">  
// HACKy  
$('body').addClass('no-sidebar');  
</script>  
 
${jsConditionalForIe(8, '&lt;script language="javascript" type="text/javascript" src="' + h.url_for_static('/scripts/vendor/flot/0.7/excanvas.js') + '"&gt;&lt;/script&gt;', 'lte')}  
<script type="text/javascript" src="${ h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js') }">//pointless jscript comment</script>  
<script type="text/javascript">  
var options = {  
xaxis: {  
mode: "time",  
timeformat: "%y-%b"  
},  
yaxis: {  
min: 0  
},  
legend: {  
position: "nw"  
}  
};  
var data = [  
[  
${",".join(c.packages_by_week)}  
]  
];  
$.plot($("#new_packages_graph"), data, options);  
</script>  
 
<script type="text/javascript">  
var options = {  
xaxis: {  
mode: "time",  
timeformat: "%y-%b"  
},  
legend: {  
position: "nw"  
},  
colors: ["#ffcc33", "#ff8844"]  
};  
var data = [  
{label: "All package revisions",  
lines: {fill: 1 },  
data: [${",".join(c.all_package_revisions)}]},  
{label: "New datasets",  
lines: {fill: 1},  
data: [${",".join(c.new_datasets)}]}  
];  
$.plot($("#package_revisions_graph"), data, options);  
</script>  
</py:def>  
<xi:include href="../../layout.html" />  
</html>  
 
<html xmlns:py="http://genshi.edgewall.org/"  
xmlns:i18n="http://genshi.edgewall.org/i18n"  
xmlns:xi="http://www.w3.org/2001/XInclude"  
py:strip="">  
 
<py:def function="page_title">Leaderboard - Stats</py:def>  
 
<py:def function="optional_head">  
<script type="text/javascript">  
var solrCoreUrl = '${c.solr_core_url}';  
</script>  
<script type="text/javascript" src="${h.url_for('/ckanext/stats/app.js')}"></script>  
<link type="text/css" rel="stylesheet" media="all" href="${h.url_for('/ckanext/stats/style.css')}" />  
</py:def>  
 
<div py:match="content">  
<h2>Dataset Leaderboard</h2>  
<p>Choose a dataset attribute and find out which categories in that area have the most datasets. E.g. tags, groups, license, res_format, country.</p>  
<form>  
<label for="category">Choose area</label>  
<input type="text" value="tags" name="attribute" id="form-attribute" />  
<input type="submit" value="Dataset Counts &raquo;" name="submit" />  
</form>  
 
<div class="category-counts">  
<ul class="chartlist" id="category-counts">  
</ul>  
</div>  
</div>  
 
<xi:include href="../../layout.html" />  
</html>  
 
 
import paste.fixture  
from pylons import config  
from ckan.config.middleware import make_app  
 
class StatsFixture(object):  
 
@classmethod  
def setup_class(cls):  
cls._original_config = config.copy()  
config['ckan.plugins'] = 'stats'  
wsgiapp = make_app(config['global_conf'], **config)  
cls.app = paste.fixture.TestApp(wsgiapp)  
 
@classmethod  
def teardown_class(cls):  
config.clear()  
config.update(cls._original_config)  
 
import datetime  
from nose.tools import assert_equal  
 
from ckan.lib.create_test_data import CreateTestData  
from ckan import model  
 
from ckanext.stats.stats import Stats, RevisionStats  
from ckanext.stats.tests import StatsFixture  
 
class TestStatsPlugin(StatsFixture):  
@classmethod  
def setup_class(cls):  
super(TestStatsPlugin, cls).setup_class()  
 
CreateTestData.create_arbitrary([  
{'name':'test1', 'groups':['grp1'], 'tags':['tag1']},  
{'name':'test2', 'groups':['grp1', 'grp2'], 'tags':['tag1']},  
{'name':'test3', 'groups':['grp1', 'grp2'], 'tags':['tag1', 'tag2']},  
{'name':'test4'},  
],  
extra_user_names=['bob'],  
admins=['bob'],  
)  
# hack revision timestamps to be this date  
week1 = datetime.datetime(2011, 1, 5)  
for rev in model.Session.query(model.Revision):  
rev.timestamp = week1 + datetime.timedelta(seconds=1)  
 
# week 2  
rev = model.repo.new_revision()  
rev.author = 'bob'  
rev.timestamp = datetime.datetime(2011, 1, 12)  
model.Package.by_name(u'test2').delete()  
model.repo.commit_and_remove()  
 
# week 3  
rev = model.repo.new_revision()  
rev.author = 'sandra'  
rev.timestamp = datetime.datetime(2011, 1, 19)  
model.Package.by_name(u'test3').title = 'Test 3'  
model.repo.commit_and_remove()  
rev = model.repo.new_revision()  
rev.author = 'sandra'  
rev.timestamp = datetime.datetime(2011, 1, 20)  
model.Package.by_name(u'test4').title = 'Test 4'  
model.repo.commit_and_remove()  
 
# week 4  
rev = model.repo.new_revision()  
rev.author = 'bob'  
rev.timestamp = datetime.datetime(2011, 1, 26)  
model.Package.by_name(u'test3').notes = 'Test 3 notes'  
model.repo.commit_and_remove()  
 
@classmethod  
def teardown_class(cls):  
CreateTestData.delete()  
 
def test_top_rated_packages(self):  
pkgs = Stats.top_rated_packages()  
assert pkgs == []  
 
def test_most_edited_packages(self):  
pkgs = Stats.most_edited_packages()  
pkgs = [(pkg.name, count) for pkg, count in pkgs]  
assert_equal(pkgs[0], ('test3', 3))  
assert_equal(pkgs[1][1], 2)  
assert_equal(pkgs[2][1], 2)  
assert_equal(pkgs[3], ('test1', 1))  
 
def test_largest_groups(self):  
grps = Stats.largest_groups()  
grps = [(grp.name, count) for grp, count in grps]  
assert_equal(grps, [('grp1', 3),  
('grp2', 2)])  
 
def test_top_tags(self):  
tags = Stats.top_tags()  
tags = [(tag.name, count) for tag, count in tags]  
assert_equal(tags, [('tag1', 3),  
('tag2', 1)])  
 
def test_top_package_owners(self):  
owners = Stats.top_package_owners()  
owners = [(owner.name, count) for owner, count in owners]  
assert_equal(owners, [('bob', 4)])  
 
def test_new_packages_by_week(self):  
new_packages_by_week = RevisionStats.get_by_week('new_packages')  
def get_results(week_number):  
date, ids, num, cumulative = new_packages_by_week[week_number]  
return (date, set([model.Session.query(model.Package).get(id).name for id in ids]), num, cumulative)  
assert_equal(get_results(0),  
('2011-01-03', set((u'test1', u'test2', u'test3', u'test4')), 4, 4))  
assert_equal(get_results(1),  
('2011-01-10', set([]), 0, 4))  
assert_equal(get_results(2),  
('2011-01-17', set([]), 0, 4))  
assert_equal(get_results(3),  
('2011-01-24', set([]), 0, 4))  
 
def test_deleted_packages_by_week(self):  
deleted_packages_by_week = RevisionStats.get_by_week('deleted_packages')  
def get_results(week_number):  
date, ids, num, cumulative = deleted_packages_by_week[week_number]  
return (date, [model.Session.query(model.Package).get(id).name for id in ids], num, cumulative)  
assert_equal(get_results(0),  
('2011-01-10', [u'test2'], 1, 1))  
assert_equal(get_results(1),  
('2011-01-17', [], 0, 1))  
assert_equal(get_results(2),  
('2011-01-24', [], 0, 1))  
assert_equal(get_results(3),  
('2011-01-31', [], 0, 1))  
 
def test_revisions_by_week(self):  
revisions_by_week = RevisionStats.get_by_week('package_revisions')  
def get_results(week_number):  
date, ids, num, cumulative = revisions_by_week[week_number]  
return (date, num, cumulative)  
num_setup_revs = revisions_by_week[0][2]  
assert 6 > num_setup_revs > 2, num_setup_revs  
assert_equal(get_results(0),  
('2011-01-03', num_setup_revs, num_setup_revs))  
assert_equal(get_results(1),  
('2011-01-10', 1, num_setup_revs+1))  
assert_equal(get_results(2),  
('2011-01-17', 2, num_setup_revs+3))  
assert_equal(get_results(3),  
('2011-01-24', 1, num_setup_revs+4))  
 
def test_num_packages_by_week(self):  
num_packages_by_week = RevisionStats.get_num_packages_by_week()  
# e.g. [('2011-05-30', 3, 3)]  
assert_equal(num_packages_by_week[0], ('2011-01-03', 4, 4))  
assert_equal(num_packages_by_week[1], ('2011-01-10', -1, 3))  
assert_equal(num_packages_by_week[2], ('2011-01-17', 0, 3))  
assert_equal(num_packages_by_week[3], ('2011-01-24', 0, 3))  
 
import os  
 
from ckan.tests import url_for  
 
from ckanext.stats.tests import StatsFixture  
 
class TestStatsPlugin(StatsFixture):  
 
def test_01_config(self):  
from pylons import config  
paths = config['extra_public_paths']  
publicdir = os.path.join(os.path.dirname(os.path.dirname(__file__)),  
'public')  
assert paths.startswith(publicdir), (publicdir, paths)  
 
def test_02_index(self):  
url = url_for('stats')  
out = self.app.get(url)  
assert 'Total number of Datasets' in out, out  
assert 'Most Edited Datasets' in out, out  
 
def test_03_leaderboard(self):  
url = url_for('stats_action', action='leaderboard')  
out = self.app.get(url)  
assert 'Leaderboard' in out, out  
 
 
  # empty file needed for pylons to find templates in this directory
 
  import ckan.plugins as p
  from ckan.lib.base import BaseController, config
  import stats as stats_lib
  import ckan.lib.helpers as h
 
  class StatsController(BaseController):
 
  def index(self):
  c = p.toolkit.c
  stats = stats_lib.Stats()
  rev_stats = stats_lib.RevisionStats()
  c.top_rated_packages = stats.top_rated_packages()
  c.most_edited_packages = stats.most_edited_packages()
  c.largest_groups = stats.largest_groups()
  c.top_tags = stats.top_tags()
  c.top_package_owners = stats.top_package_owners()
  c.summary_stats = stats.summary_stats()
  c.activity_counts = stats.activity_counts()
  c.by_org = stats.by_org()
  c.res_by_org = stats.res_by_org()
  c.top_active_orgs = stats.top_active_orgs()
  c.user_access_list = stats.user_access_list()
  c.recent_datasets = stats.recent_datasets()
  c.new_packages_by_week = rev_stats.get_by_week('new_packages')
  c.deleted_packages_by_week = rev_stats.get_by_week('deleted_packages')
  c.num_packages_by_week = rev_stats.get_num_packages_by_week()
  c.package_revisions_by_week = rev_stats.get_by_week('package_revisions')
 
  # Used in the legacy CKAN templates.
  c.packages_by_week = []
 
  # Used in new CKAN templates gives more control to the templates for formatting.
  c.raw_packages_by_week = []
  for week_date, num_packages, cumulative_num_packages in c.num_packages_by_week:
  c.packages_by_week.append('[new Date(%s), %s]' % (week_date.replace('-', ','), cumulative_num_packages))
  c.raw_packages_by_week.append({'date': h.date_str_to_datetime(week_date), 'total_packages': cumulative_num_packages})
 
  c.all_package_revisions = []
  c.raw_all_package_revisions = []
  for week_date, revs, num_revisions, cumulative_num_revisions in c.package_revisions_by_week:
  c.all_package_revisions.append('[new Date(%s), %s]' % (week_date.replace('-', ','), num_revisions))
  c.raw_all_package_revisions.append({'date': h.date_str_to_datetime(week_date), 'total_revisions': num_revisions})
 
  c.new_datasets = []
  c.raw_new_datasets = []
  for week_date, pkgs, num_packages, cumulative_num_packages in c.new_packages_by_week:
  c.new_datasets.append('[new Date(%s), %s]' % (week_date.replace('-', ','), num_packages))
  c.raw_new_datasets.append({'date': h.date_str_to_datetime(week_date), 'new_packages': num_packages})
 
  return p.toolkit.render('ckanext/stats/index.html')
 
  def leaderboard(self, id=None):
  c = p.toolkit.c
  c.solr_core_url = config.get('ckanext.stats.solr_core_url',
  'http://solr.okfn.org/solr/ckan')
  return p.toolkit.render('ckanext/stats/leaderboard.html')
 
 
  from logging import getLogger
 
  import ckan.plugins as p
 
  log = getLogger(__name__)
 
  class StatsPlugin(p.SingletonPlugin):
  '''Stats plugin.'''
 
  p.implements(p.IRoutes, inherit=True)
  p.implements(p.IConfigurer, inherit=True)
 
  def after_map(self, map):
  map.connect('stats', '/stats',
  controller='ckanext.dga_stats.controller:StatsController',
  action='index')
  map.connect('stats_action', '/stats/{action}',
  controller='ckanext.dga_stats.controller:StatsController')
  return map
 
  def update_config(self, config):
  templates = 'templates'
  if p.toolkit.asbool(config.get('ckan.legacy_templates', False)):
  templates = 'templates_legacy'
  p.toolkit.add_template_directory(config, templates)
  p.toolkit.add_public_directory(config, 'public')
  p.toolkit.add_resource('public/ckanext/stats', 'ckanext_dga_stats')
 
  **.min.js
  **.min.css
 
  # this is a namespace package
  try:
  import pkg_resources
  pkg_resources.declare_namespace(__name__)
  except ImportError:
  import pkgutil
  __path__ = pkgutil.extend_path(__path__, __name__)
 
  # this is a namespace package
  try:
  import pkg_resources
  pkg_resources.declare_namespace(__name__)
  except ImportError:
  import pkgutil
  __path__ = pkgutil.extend_path(__path__, __name__)
 
  # this is a namespace package
  try:
  import pkg_resources
  pkg_resources.declare_namespace(__name__)
  except ImportError:
  import pkgutil
  __path__ = pkgutil.extend_path(__path__, __name__)
 
  jQuery(document).ready(function($) {
  $('form').submit(function(e) {
  e.preventDefault();
  attribute = $('#form-attribute').val();
  loadSolr(attribute);
  })
  // default! (also in html)
  loadSolr('tags');
 
  function loadSolr(attribute) {
  var url = solrCoreUrl + '/select?indent=on&wt=json&facet=true&rows=0&indent=true&facet.mincount=1&facet.limit=30&q=*:*&facet.field=' + attribute;
  function handleSolr(data) {
  var results = [];
  ourdata = data.facet_counts.facet_fields[attribute];
  var newrow = {};
  for (ii in ourdata) {
  if (ii % 2 == 0) {
  newrow.name = ourdata[ii];
  if (!newrow.name) {
  newrow.name = '[Not Specified]';
  }
  } else {
  newrow.count = ourdata[ii];
  results.push(newrow);
  newrow = {};
  }
  }
  display(results);
  }
 
  $.ajax({
  url: url,
  success: handleSolr,
  dataType: 'jsonp',
  jsonp: 'json.wrf'
  });
  }
 
  function display(results) {
  var list = $('#category-counts');
  list.html('');
  if (results.length == 0) {
  return
  }
  var maximum = results[0]['count'];
  for(ii in results) {
  maximum = Math.max(maximum, results[ii]['count']);
  }
 
  $.each(results, function(idx, row) {
  var newentry = $('<li></li>');
  newentry.append($('<a href="#">' + row['name'] + '</a>'));
  newentry.append($('<span class="count">' + row['count'] + '</a>'));
  var percent = 100 * row['count'] / maximum;
  newentry.append($('<span class="index" style="width: ' + percent + '%"></span>'));
  list.append(newentry);
  });
  }
  });
 
  .tab-content h2 {
  margin-bottom: 12px;
  }
 
  .js .tab-content {
  padding-top: 20px;
  padding-bottom: 20px;
  margin-top: 0;
  }
 
  .module-plot-canvas {
  display: block;
  width: 650px;
  height: 300px;
  margin: 20px 0;
  }
 
  <html>
  <head>
  <script type="text/javascript">
  var solrCoreUrl = 'http://solr.okfn.org/solr/ckan';
  </script>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <script type="text/javascript" src="app.js"></script>
 
  <link type="text/css" rel="stylesheet" media="all" href="style.css" />
  </head>
  <body>
  <h1>CKAN Dataset Leaderboard</h1>
  <p>Choose a dataset attribute and find out which categories in that area have the most datasets. E.g. tags, groups, license, res_format, country.</p>
  <form>
  <label for="category">Choose area</label>
  <input type="text" value="tags" name="attribute" id="form-attribute" />
  <input type="submit" value="Dataset Counts &raquo;" name="submit" />
  </form>
 
  <div class="category-counts">
  <ul class="chartlist" id="category-counts">
  </ul>
  </div>
  </body>
  </html>
 
  /* A quick module for generating flot charts from an HTML table. Options can
  * be passed directly to flot using the data-module-* attributes. The tables
  * are currently expected to be marked up as follows:
  *
  * <table data-module="plot">
  * <thead>
  * <tr>
  * <th>X Axis</th>
  * <th>Series A Legend</th>
  * <th>Series B Legend</th>
  * </tr>
  * </thead>
  * <tbody>
  * <tr>
  * <th>X Value</th>
  * <td>Series A Y Value</td>
  * <td>Series B Y Value</td>
  * </tr>
  * ...
  * </tbody>
  * </table>
  *
  * Points are pulled out of the th/td elements using innerHTML or by looking
  * for a data-value attribute. This is useful when a more readable value
  * needs to be used in the elements contents (eg. dates). A data-type attribute
  * can also be applied to parse the value. Only data-type="date" is currently
  * supported and expects data-value to be a unix timestamp.
  */
  this.ckan.module('plot', function (jQuery, _) {
  return {
  /* Holds the jQuery.plot() object when created */
  graph: null,
 
  /* Holds the canvas container when created */
  canvas: null,
 
  /* Default options */
  options: {
  xaxis: {},
  yaxis: {},
  legend: {position: 'nw'},
  colors: ['#ffcc33', '#ff8844']
  },
 
  /* Sets up the canvas element and parses the table.
  *
  * Returns nothing.
  */
  initialize: function () {
  jQuery.proxyAll(this, /_on/);
 
  if (!this.el.is('table')) {
  throw new Error('CKAN module plot can only be called on table elements');
  }
 
  this.setupCanvas();
 
  // Because the canvas doesn't render correctly unless visible we must
  // listen for events that reveal the canvas and then try and re-render.
  // Currently the most common of these is the "shown" event triggered by
  // the tabs plugin.
  this.sandbox.body.on("shown", this._onShown);
  this.data = this.parseTable(this.el);
 
  this.draw();
  },
 
  /* Removes event handlers when the module is removed from the DOM.
  *
  * Returns nothing.
  */
  teardown: function () {
  this.sandbox.body.off("shown", this._onShown);
  },
 
  /* Creates the canvas wrapper and removes the table from the document.
  *
  * Returns nothing.
  */
  setupCanvas: function () {
  this.canvas = jQuery('<div class="module-plot-canvas">');
  this.el.replaceWith(this.canvas);
  },
 
  /* Attempts to draw the chart if the canvas is visible. If not visible the
  * graph does not render correctly. So we keep trying until it is.
  *
  * Examples
  *
  * module.draw();
  *
  * Returns nothing.
  */
  draw: function () {
  if (!this.drawn && this.canvas.is(':visible')) {
  this.graph = jQuery.plot(this.canvas, this.data, this.options);
  }
  },
 
  /* Parses an HTML table element to build the data array for the chart.
  * The thead element provides the axis and labels for the series. The
  * first column in the tbody is used for the x-axis and subsequent
  * columns are the series.
  *
  * table - A table element to parse.
  *
  * Examples
  *
  * module.parseTable(module.el);
  *
  * Returns data object suitable for use in jQuery.plot().
  */
  parseTable: function (table) {
  var data = [];
  var _this = this;
 
  var headings = table.find('thead tr:first th').map(function () {
  return this.innerHTML;
  });
 
  table.find('tbody tr').each(function (row) {
  var element = jQuery(this);
  var x = [];
 
  x[row] = _this.getValue(element.find('th'));
 
  element.find('td').each(function (series) {
  var value = _this.getValue(this);
  var label = headings[series + 1];
 
  data[series] = data[series] || {data: [], label: label};
  data[series].data[row] = [x[row], value];
  });
  });
 
  return data;
  },
 
  /* Retrieves the value from a td/th element. This first looks for a
  * data-value attribute on the element otherwise uses the element
  * text contents.
  *
  * A data-type attribute can also be provided to tell the module how
  * to deal with the element. By default we let jQuery.data() handle
  * the parsing but this can provide additional data. See .parseValue()
  * for more info.
  *
  * cell - An element to extract a value from.
  *
  * Examples
  *
  * var element = jQuery('<td data-value="10">Ten</td>');
  * module.getValue(element); //=> 10
  *
  * var element = jQuery('<td>20</td>');
  * module.getValue(element); //=> 20
  *
  * var element = jQuery('<td data-type="date">1343747094</td>');
  * module.getValue(element); //=> <Date Tue Jul 31 2012 16:04:54 GMT+0100 (BST)>
  *
  * Returns the parsed value.
  */
  getValue: function (cell) {
  var item = cell instanceof jQuery ? cell : jQuery(cell);
  var type = item.data('type') || 'string';
  var value = item.data('value') || item.text();
  return this.parseValue(value, type);
  },
 
  /* Provides the ability to further format a value.
  *
  * If date is provided as a type then it expects value to be a unix
  * timestamp in seconds.
  *
  * value - The value extracted from the element.
  * type - A type string, currently only supports "date".
  *
  * Examples
  *
  * module.parseValue(10); // => 10
  * module.parseValue("cat"); // => "cat"
  * module.parseValue(1343747094, 'date'); // => <Date Tue Jul 31 2012 16:04:54 GMT+0100 (BST)>
  *
  * Returns the parsed value.
  */
  parseValue: function (value, type) {
  if (type === 'date') {
  value = new Date(parseInt(value, 10) * 1000);
  if (!value) {
  value = 0;
  }
  }
  return value;
  },
 
  /* Event handler for when tabs are toggled. Determines if the canvas
  * resides in the shown element and attempts to re-render.
  *
  * event - The shown event object.
  *
  * Returns nothing.
  */
  _onShown: function (event) {
  if (!this.drawn && jQuery.contains(jQuery(event.target.hash)[0], this.canvas[0])) {
  this.draw();
  }
  }
  };
  });
 
  /* Quick module to enhance the Bootstrap tags plug-in to update the url
  * hash when a tab changes to allow the user to bookmark the page.
  *
  * Each tab id must use a prefix which which will be stripped from the hash.
  * This is to prevent the page jumping when the hash fragment changes.
  *
  * prefix - The prefix used on the ids.
  *
  */
  this.ckan.module('stats-nav', {
  /* An options object */
  options: {
  prefix: 'stats-'
  },
 
  /* Initializes the module and sets up event listeners.
  *
  * Returns nothing.
  */
  initialize: function () {
  var location = this.sandbox.location;
  var prefix = this.options.prefix;
  var hash = location.hash.slice(1);
  var selected = this.$('[href^=#' + prefix + hash + ']');
 
  // Update the hash fragment when the tab changes.
  this.el.on('shown', function (event) {
  location.hash = event.target.hash.slice(prefix.length + 1);
  });
 
  // Show the current tab if the location provides one.
  if (selected.length) {
  selected.tab('show');
  }
  }
  });
 
  [IE conditional]
 
  lte IE 8 = vendor/excanvas.js
 
  [groups]
 
  stats =
  css/stats.css
  vendor/excanvas.js
  vendor/jquery.flot.js
  javascript/modules/plot.js
  javascript/modules/stats-nav.js
 
  div.category-counts {
  }
 
  div.category-counts-over-time {
  clear: both;
  }
 
  /***************************
  * CHART LISTS
  **************************/
 
  .chartlist {
  float: left;
  border-top: 1px solid #EEE;
  width: 90%;
  padding-left: 0;
  margin-left: 0;
  }
 
  .chartlist li {
  position: relative;
  display: block;
  border-bottom: 1px solid #EEE;
  _zoom: 1;
  }
  .chartlist li a {
  display: block;
  padding: 0.4em 4.5em 0.4em 0.5em;
  position: relative;
  z-index: 2;
  }
  .chartlist .count {
  display: block;
  position: absolute;
  top: 0;
  right: 0;
  margin: 0 0.3em;
  text-align: right;
  color: #999;
  font-weight: bold;
  font-size: 0.875em;
  line-height: 2em;
  z-index: 999;
  }
  .chartlist .index {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background: #B8E4F5;
  text-indent: -9999px;
  overflow: hidden;
  line-height: 2em;
  }
  .chartlist li:hover {
  background: #EFEFEF;
  }
 
 
  <table data-module="plot">
  <thead>
  <tr>
  <th>X Axis</th>
  <th>Series A Legend</th>
  <th>Series B Legend</th>
  </tr>
  </thead>
  <tbody>
  <tr>
  <!-- This is the x value for each series -->
  <th data-type="date" data-value="1176073200">Apr 09, 2007</th>
  <!-- This is the y value for series a -->
  <td>20</td>
  <!-- This is the y value for series b -->
  <td>7</td>
  </tr>
  <tr>
  <th data-type="date" data-value="1176678000">Apr 16, 2007</th>
  <td>12</td>
  <td>6</td>
  </tr>
  <tr>
  <th data-type="date" data-value="1177282800">Apr 23, 2007</th>
  <td>27</td>
  <td>12</td>
  </tr>
  </tbody>
  </table>
 
 
  <!DOCTYPE html>
  <html>
  <head>
  <meta charset="utf-8" />
  <title>Mocha Tests</title>
  <link rel="stylesheet" href="../../../base/test/vendor/mocha.css" />
  </head>
  <body>
  <div id="mocha"></div>
  <div id="fixture" style="position: absolute; top: -9999px; left: -9999px"></div>
 
  <!-- Test Runner -->
  <script src="../../../base/test/vendor/sinon.js"></script>
  <script src="../../../base/test/vendor/mocha.js"></script>
  <script src="../../../base/test/vendor/chai.js"></script>
  <script>
  mocha.setup('bdd');
  var assert = chai.assert;
 
  // Export sinon.assert methods onto assert.
  sinon.assert.expose(assert, {prefix: ''});
 
  var ckan = {ENV: 'testing'};
  </script>
 
  <!-- Source -->
  <script src="../../../base/vendor/jed.js"></script>
  <script src="../../../base/vendor/jquery.js"></script>
  <script src="../../../base/vendor/bootstrap/js/bootstrap-transition.js"></script>
  <script src="../../../base/vendor/bootstrap/js/bootstrap-alert.js"></script>
  <script src="../../../base/javascript/plugins/jquery.inherit.js"></script>
  <script src="../../../base/javascript/plugins/jquery.proxy-all.js"></script>
  <script src="../../../base/javascript/sandbox.js"></script>
  <script src="../../../base/javascript/module.js"></script>
  <script src="../../../base/javascript/pubsub.js"></script>
  <script src="../../../base/javascript/client.js"></script>
  <script src="../../../base/javascript/notify.js"></script>
  <script src="../../../base/javascript/i18n.js"></script>
  <script src="../../../base/javascript/main.js"></script>
  <script src="../javascript/modules/plot.js"></script>
  <script src="../javascript/modules/stats-nav.js"></script>
 
  <!-- Suite -->
  <script src="./spec/modules/plot.spec.js"></script>
  <script src="./spec/modules/stats-nav.spec.js"></script>
 
  <script>
  beforeEach(function () {
  this.fixture = jQuery('#fixture').empty();
  });
 
  afterEach(function () {
  this.fixture.empty();
  });
 
  mocha.run().globals(['ckan', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']);
  </script>
  </body>
  </html>
 
  /*globals describe before beforeEach afterEach it assert sinon ckan jQuery */
  describe('ckan.module.PlotModule()', function () {
  var PlotModule = ckan.module.registry['plot'];
 
  before(function (done) {
  var _this = this;
 
  jQuery.get('./fixtures/table.html', function (html) {
  _this.template = html;
  done();
  });
  });
 
  beforeEach(function () {
  this.el = jQuery(this.template).appendTo(this.fixture);
  this.sandbox = ckan.sandbox();
  this.sandbox.body = this.fixture;
  this.module = new PlotModule(this.el, {}, this.sandbox);
  });
 
  afterEach(function () {
  this.module.teardown();
  });
 
  describe('.initialize()', function () {
  it('should setup the canvas element', function () {
  var target = sinon.stub(this.module, 'setupCanvas', this.module.setupCanvas);
 
  this.module.initialize();
  assert.called(target);
  });
 
  it('should draw the graph', function () {
  var target = sinon.stub(this.module, 'draw');
 
  this.module.initialize();
  assert.called(target);
  });
 
  it('should listen for "shown" events on the body', function () {
  var target = sinon.stub(this.sandbox.body, 'on');
 
  this.module.initialize();
  assert.called(target);
  assert.calledWith(target, "shown", this.module._onShown);
  });
  });
 
  describe('.teardown()', function () {
  it('should remove "shown" listeners from the body', function () {
  var target = sinon.stub(this.sandbox.body, 'off');
 
  this.module.teardown();
  assert.called(target);
  assert.calledWith(target, "shown", this.module._onShown);
  });
  });
 
  describe('.setupCanvas()', function () {
  it('should create the .canvas element', function () {
  this.module.setupCanvas();
 
  assert.isDefined(this.module.canvas);
  assert.isDefined(this.module.canvas.is('div'));
  });
 
  it('should replace the .el with the .canvas', function () {
  this.module.setupCanvas();
  assert.ok(jQuery.contains(this.sandbox.body[0], this.module.canvas[0]));
  });
  });
 
  describe('.draw()', function () {
  beforeEach(function () {
  this.plot = {};
  this.module.canvas = jQuery('<div />').appendTo(this.fixture);
  jQuery.plot = sinon.stub().returns(this.plot);
  });
 
  it('should call jQuery.plot() if the canvas is visible', function () {
  this.module.draw();
 
  assert.called(jQuery.plot);
  assert.calledWith(jQuery.plot, this.module.canvas, this.module.data, this.module.options);
  });
 
  it('should assign the .graph property', function () {
  this.module.draw();
  assert.strictEqual(this.module.graph, this.plot);
  });
 
  it('should not call jQuery.plot() if the canvas is not visible', function () {
  this.module.canvas.hide();
  this.module.draw();
 
  assert.notCalled(jQuery.plot);
  });
  });
 
  describe('.parseTable(table)', function () {
  it('should parse the contents of the provided table', function () {
  var target = this.module.parseTable(this.module.el);
 
  assert.deepEqual(target, [{
  label: 'Series A Legend',
  data: [
  [new Date(1176073200000), "20"],
  [new Date(1176678000000), "12"],
  [new Date(1177282800000), "27"]
  ]
  }, {
  label: 'Series B Legend',
  data: [
  [new Date(1176073200000), "7"],
  [new Date(1176678000000), "6"],
  [new Date(1177282800000), "12"]
  ]
  }]);
  });
  });
 
  describe('.getValue(cell)', function () {
  it('should extract the value from a table cell');
  it('should use the data-value attribute if present');
  it('should parse the value using the data-type');
  });
 
  describe('.parseValue(value, type)', function () {
  it('should create a date object if type == "date"');
  it('should return the value if the type is not recognised');
  });
 
  describe('._onShown(event)', function () {
  it('should call .draw() if the event.target contains the canvas');
  });
  });
 
  /*globals describe before beforeEach afterEach it assert sinon ckan jQuery */
  describe('ckan.module.StatsNavModule()', function () {
  var StatsNavModule = ckan.module.registry['stats-nav'];
 
  beforeEach(function () {
  this.el = document.createElement('div');
  this.sandbox = ckan.sandbox();
  this.sandbox.body = this.fixture;
  this.sandbox.location = {
  href: '',
  hash: ''
  };
  this.module = new StatsNavModule(this.el, {}, this.sandbox);
 
  jQuery.fn.tab = sinon.stub();
  });
 
  afterEach(function () {
  this.module.teardown();
 
  delete jQuery.fn.tab;
  });
 
  describe('.initialize()', function () {
  it('should listen for shown events and update the location.hash', function () {
  var anchor = jQuery('<a />').attr('href', '#stats-test')[0];
 
  this.module.initialize();
  this.module.el.trigger({type: 'shown', target: anchor});
 
  assert.equal(this.sandbox.location.hash, 'test');
  });
 
  it('should select the tab from the location hash on init', function () {
  var anchor = jQuery('<a />').attr('href', '#stats-test').appendTo(this.el);
 
  this.sandbox.location.hash = '#test';
  this.module.initialize();
 
  assert.called(jQuery.fn.tab);
  assert.calledWith(jQuery.fn.tab, 'show');
  });
  });
  });
 
  // Copyright 2006 Google Inc.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  // http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
 
 
  // Known Issues:
  //
  // * Patterns only support repeat.
  // * Radial gradient are not implemented. The VML version of these look very
  // different from the canvas one.
  // * Clipping paths are not implemented.
  // * Coordsize. The width and height attribute have higher priority than the
  // width and height style values which isn't correct.
  // * Painting mode isn't implemented.
  // * Canvas width/height should is using content-box by default. IE in
  // Quirks mode will draw the canvas using border-box. Either change your
  // doctype to HTML5
  // (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
  // or use Box Sizing Behavior from WebFX
  // (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
  // * Non uniform scaling does not correctly scale strokes.
  // * Filling very large shapes (above 5000 points) is buggy.
  // * Optimize. There is always room for speed improvements.
 
  // Only add this code if we do not already have a canvas implementation
  if (!document.createElement('canvas').getContext) {
 
  (function() {
 
  // alias some functions to make (compiled) code shorter
  var m = Math;
  var mr = m.round;
  var ms = m.sin;
  var mc = m.cos;
  var abs = m.abs;
  var sqrt = m.sqrt;
 
  // this is used for sub pixel precision
  var Z = 10;
  var Z2 = Z / 2;
 
  /**
  * This funtion is assigned to the <canvas> elements as element.getContext().
  * @this {HTMLElement}
  * @return {CanvasRenderingContext2D_}
  */
  function getContext() {
  return this.context_ ||
  (this.context_ = new CanvasRenderingContext2D_(this));
  }
 
  var slice = Array.prototype.slice;
 
  /**
  * Binds a function to an object. The returned function will always use the
  * passed in {@code obj} as {@code this}.
  *
  * Example:
  *
  * g = bind(f, obj, a, b)
  * g(c, d) // will do f.call(obj, a, b, c, d)
  *
  * @param {Function} f The function to bind the object to
  * @param {Object} obj The object that should act as this when the function
  * is called
  * @param {*} var_args Rest arguments that will be used as the initial
  * arguments when the function is called
  * @return {Function} A new function that has bound this
  */
  function bind(f, obj, var_args) {
  var a = slice.call(arguments, 2);
  return function() {
  return f.apply(obj, a.concat(slice.call(arguments)));
  };
  }
 
  function encodeHtmlAttribute(s) {
  return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
  }
 
  function addNamespacesAndStylesheet(doc) {
  // create xmlns
  if (!doc.namespaces['g_vml_']) {
  doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
  '#default#VML');
 
  }
  if (!doc.namespaces['g_o_']) {
  doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
  '#default#VML');
  }
 
  // Setup default CSS. Only add one style sheet per document
  if (!doc.styleSheets['ex_canvas_']) {
  var ss = doc.createStyleSheet();
  ss.owningElement.id = 'ex_canvas_';
  ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
  // default size is 300x150 in Gecko and Opera
  'text-align:left;width:300px;height:150px}';
  }
  }
 
  // Add namespaces and stylesheet at startup.
  addNamespacesAndStylesheet(document);
 
  var G_vmlCanvasManager_ = {
  init: function(opt_doc) {
  if (/MSIE/.test(navigator.userAgent) && !window.opera) {
  var doc = opt_doc || document;
  // Create a dummy element so that IE will allow canvas elements to be
  // recognized.
  doc.createElement('canvas');
  doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
  }
  },
 
  init_: function(doc) {
  // find all canvas elements
  var els = doc.getElementsByTagName('canvas');
  for (var i = 0; i < els.length; i++) {
  this.initElement(els[i]);
  }
  },
 
  /**
  * Public initializes a canvas element so that it can be used as canvas
  * element from now on. This is called automatically before the page is
  * loaded but if you are creating elements using createElement you need to
  * make sure this is called on the element.
  * @param {HTMLElement} el The canvas element to initialize.
  * @return {HTMLElement} the element that was created.
  */
  initElement: function(el) {
  if (!el.getContext) {
  el.getContext = getContext;
 
  // Add namespaces and stylesheet to document of the element.
  addNamespacesAndStylesheet(el.ownerDocument);
 
  // Remove fallback content. There is no way to hide text nodes so we
  // just remove all childNodes. We could hide all elements and remove
  // text nodes but who really cares about the fallback content.
  el.innerHTML = '';
 
  // do not use inline function because that will leak memory
  el.attachEvent('onpropertychange', onPropertyChange);
  el.attachEvent('onresize', onResize);
 
  var attrs = el.attributes;
  if (attrs.width && attrs.width.specified) {
  // TODO: use runtimeStyle and coordsize
  // el.getContext().setWidth_(attrs.width.nodeValue);
  el.style.width = attrs.width.nodeValue + 'px';
  } else {
  el.width = el.clientWidth;
  }
  if (attrs.height && attrs.height.specified) {
  // TODO: use runtimeStyle and coordsize
  // el.getContext().setHeight_(attrs.height.nodeValue);
  el.style.height = attrs.height.nodeValue + 'px';
  } else {
  el.height = el.clientHeight;
  }
  //el.getContext().setCoordsize_()
  }
  return el;
  }
  };
 
  function onPropertyChange(e) {
  var el = e.srcElement;
 
  switch (e.propertyName) {
  case 'width':
  el.getContext().clearRect();
  el.style.width = el.attributes.width.nodeValue + 'px';
  // In IE8 this does not trigger onresize.
  el.firstChild.style.width = el.clientWidth + 'px';
  break;
  case 'height':
  el.getContext().clearRect();
  el.style.height = el.attributes.height.nodeValue + 'px';
  el.firstChild.style.height = el.clientHeight + 'px';
  break;
  }
  }
 
  function onResize(e) {
  var el = e.srcElement;
  if (el.firstChild) {
  el.firstChild.style.width = el.clientWidth + 'px';
  el.firstChild.style.height = el.clientHeight + 'px';
  }
  }
 
  G_vmlCanvasManager_.init();
 
  // precompute "00" to "FF"
  var decToHex = [];
  for (var i = 0; i < 16; i++) {
  for (var j = 0; j < 16; j++) {
  decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
  }
  }
 
  function createMatrixIdentity() {
  return [
  [1, 0, 0],
  [0, 1, 0],
  [0, 0, 1]
  ];
  }
 
  function matrixMultiply(m1, m2) {
  var result = createMatrixIdentity();
 
  for (var x = 0; x < 3; x++) {
  for (var y = 0; y < 3; y++) {
  var sum = 0;
 
  for (var z = 0; z < 3; z++) {
  sum += m1[x][z] * m2[z][y];
  }
 
  result[x][y] = sum;
  }
  }
  return result;
  }
 
  function copyState(o1, o2) {
  o2.fillStyle = o1.fillStyle;
  o2.lineCap = o1.lineCap;
  o2.lineJoin = o1.lineJoin;
  o2.lineWidth = o1.lineWidth;
  o2.miterLimit = o1.miterLimit;
  o2.shadowBlur = o1.shadowBlur;
  o2.shadowColor = o1.shadowColor;
  o2.shadowOffsetX = o1.shadowOffsetX;
  o2.shadowOffsetY = o1.shadowOffsetY;
  o2.strokeStyle = o1.strokeStyle;
  o2.globalAlpha = o1.globalAlpha;
  o2.font = o1.font;
  o2.textAlign = o1.textAlign;
  o2.textBaseline = o1.textBaseline;
  o2.arcScaleX_ = o1.arcScaleX_;
  o2.arcScaleY_ = o1.arcScaleY_;
  o2.lineScale_ = o1.lineScale_;
  }
 
  var colorData = {
  aliceblue: '#F0F8FF',
  antiquewhite: '#FAEBD7',
  aquamarine: '#7FFFD4',
  azure: '#F0FFFF',
  beige: '#F5F5DC',
  bisque: '#FFE4C4',
  black: '#000000',
  blanchedalmond: '#FFEBCD',
  blueviolet: '#8A2BE2',
  brown: '#A52A2A',
  burlywood: '#DEB887',
  cadetblue: '#5F9EA0',
  chartreuse: '#7FFF00',
  chocolate: '#D2691E',
  coral: '#FF7F50',
  cornflowerblue: '#6495ED',
  cornsilk: '#FFF8DC',
  crimson: '#DC143C',
  cyan: '#00FFFF',
  darkblue: '#00008B',
  darkcyan: '#008B8B',
  darkgoldenrod: '#B8860B',
  darkgray: '#A9A9A9',
  darkgreen: '#006400',
  darkgrey: '#A9A9A9',
  darkkhaki: '#BDB76B',
  darkmagenta: '#8B008B',
  darkolivegreen: '#556B2F',
  darkorange: '#FF8C00',
  darkorchid: '#9932CC',
  darkred: '#8B0000',
  darksalmon: '#E9967A',
  darkseagreen: '#8FBC8F',
  darkslateblue: '#483D8B',
  darkslategray: '#2F4F4F',
  darkslategrey: '#2F4F4F',
  darkturquoise: '#00CED1',
  darkviolet: '#9400D3',
  deeppink: '#FF1493',
  deepskyblue: '#00BFFF',
  dimgray: '#696969',
  dimgrey: '#696969',
  dodgerblue: '#1E90FF',
  firebrick: '#B22222',
  floralwhite: '#FFFAF0',
  forestgreen: '#228B22',
  gainsboro: '#DCDCDC',
  ghostwhite: '#F8F8FF',
  gold: '#FFD700',
  goldenrod: '#DAA520',
  grey: '#808080',
  greenyellow: '#ADFF2F',
  honeydew: '#F0FFF0',
  hotpink: '#FF69B4',
  indianred: '#CD5C5C',
  indigo: '#4B0082',
  ivory: '#FFFFF0',
  khaki: '#F0E68C',
  lavender: '#E6E6FA',
  lavenderblush: '#FFF0F5',
  lawngreen: '#7CFC00',
  lemonchiffon: '#FFFACD',
  lightblue: '#ADD8E6',
  lightcoral: '#F08080',
  lightcyan: '#E0FFFF',
  lightgoldenrodyellow: '#FAFAD2',
  lightgreen: '#90EE90',
  lightgrey: '#D3D3D3',
  lightpink: '#FFB6C1',
  lightsalmon: '#FFA07A',
  lightseagreen: '#20B2AA',
  lightskyblue: '#87CEFA',
  lightslategray: '#778899',
  lightslategrey: '#778899',
  lightsteelblue: '#B0C4DE',
  lightyellow: '#FFFFE0',
  limegreen: '#32CD32',
  linen: '#FAF0E6',
  magenta: '#FF00FF',
  mediumaquamarine: '#66CDAA',
  mediumblue: '#0000CD',
  mediumorchid: '#BA55D3',
  mediumpurple: '#9370DB',
  mediumseagreen: '#3CB371',
  mediumslateblue: '#7B68EE',
  mediumspringgreen: '#00FA9A',
  mediumturquoise: '#48D1CC',
  mediumvioletred: '#C71585',
  midnightblue: '#191970',
  mintcream: '#F5FFFA',
  mistyrose: '#FFE4E1',
  moccasin: '#FFE4B5',
  navajowhite: '#FFDEAD',
  oldlace: '#FDF5E6',
  olivedrab: '#6B8E23',
  orange: '#FFA500',
  orangered: '#FF4500',
  orchid: '#DA70D6',
  palegoldenrod: '#EEE8AA',
  palegreen: '#98FB98',
  paleturquoise: '#AFEEEE',
  palevioletred: '#DB7093',
  papayawhip: '#FFEFD5',
  peachpuff: '#FFDAB9',
  peru: '#CD853F',
  pink: '#FFC0CB',
  plum: '#DDA0DD',
  powderblue: '#B0E0E6',
  rosybrown: '#BC8F8F',
  royalblue: '#4169E1',
  saddlebrown: '#8B4513',
  salmon: '#FA8072',
  sandybrown: '#F4A460',
  seagreen: '#2E8B57',
  seashell: '#FFF5EE',
  sienna: '#A0522D',
  skyblue: '#87CEEB',
  slateblue: '#6A5ACD',
  slategray: '#708090',
  slategrey: '#708090',
  snow: '#FFFAFA',
  springgreen: '#00FF7F',
  steelblue: '#4682B4',
  tan: '#D2B48C',
  thistle: '#D8BFD8',
  tomato: '#FF6347',
  turquoise: '#40E0D0',
  violet: '#EE82EE',
  wheat: '#F5DEB3',
  whitesmoke: '#F5F5F5',
  yellowgreen: '#9ACD32'
  };
 
 
  function getRgbHslContent(styleString) {
  var start = styleString.indexOf('(', 3);
  var end = styleString.indexOf(')', start + 1);
  var parts = styleString.substring(start + 1, end).split(',');
  // add alpha if needed
  if (parts.length == 4 && styleString.substr(3, 1) == 'a') {
  alpha = Number(parts[3]);
  } else {
  parts[3] = 1;
  }
  return parts;
  }
 
  function percent(s) {
  return parseFloat(s) / 100;
  }
 
  function clamp(v, min, max) {
  return Math.min(max, Math.max(min, v));
  }
 
  function hslToRgb(parts){
  var r, g, b;
  h = parseFloat(parts[0]) / 360 % 360;
  if (h < 0)
  h++;
  s = clamp(percent(parts[1]), 0, 1);
  l = clamp(percent(parts[2]), 0, 1);
  if (s == 0) {
  r = g = b = l; // achromatic
  } else {
  var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  var p = 2 * l - q;
  r = hueToRgb(p, q, h + 1 / 3);
  g = hueToRgb(p, q, h);
  b = hueToRgb(p, q, h - 1 / 3);
  }
 
  return '#' + decToHex[Math.floor(r * 255)] +
  decToHex[Math.floor(g * 255)] +
  decToHex[Math.floor(b * 255)];
  }
 
  function hueToRgb(m1, m2, h) {
  if (h < 0)
  h++;
  if (h > 1)
  h--;
 
  if (6 * h < 1)
  return m1 + (m2 - m1) * 6 * h;
  else if (2 * h < 1)
  return m2;
  else if (3 * h < 2)
  return m1 + (m2 - m1) * (2 / 3 - h) * 6;
  else
  return m1;
  }
 
  function processStyle(styleString) {
  var str, alpha = 1;
 
  styleString = String(styleString);
  if (styleString.charAt(0) == '#') {
  str = styleString;
  } else if (/^rgb/.test(styleString)) {
  var parts = getRgbHslContent(styleString);
  var str = '#', n;
  for (var i = 0; i < 3; i++) {
  if (parts[i].indexOf('%') != -1) {
  n = Math.floor(percent(parts[i]) * 255);
  } else {
  n = Number(parts[i]);
  }
  str += decToHex[clamp(n, 0, 255)];
  }
  alpha = parts[3];
  } else if (/^hsl/.test(styleString)) {
  var parts = getRgbHslContent(styleString);
  str = hslToRgb(parts);
  alpha = parts[3];
  } else {
  str = colorData[styleString] || styleString;
  }
  return {color: str, alpha: alpha};
  }
 
  var DEFAULT_STYLE = {
  style: 'normal',
  variant: 'normal',
  weight: 'normal',
  size: 10,
  family: 'sans-serif'
  };
 
  // Internal text style cache
  var fontStyleCache = {};
 
  function processFontStyle(styleString) {
  if (fontStyleCache[styleString]) {
  return fontStyleCache[styleString];
  }
 
  var el = document.createElement('div');
  var style = el.style;
  try {
  style.font = styleString;
  } catch (ex) {
  // Ignore failures to set to invalid font.
  }
 
  return fontStyleCache[styleString] = {
  style: style.fontStyle || DEFAULT_STYLE.style,
  variant: style.fontVariant || DEFAULT_STYLE.variant,
  weight: style.fontWeight || DEFAULT_STYLE.weight,
  size: style.fontSize || DEFAULT_STYLE.size,
  family: style.fontFamily || DEFAULT_STYLE.family
  };
  }
 
  function getComputedStyle(style, element) {
  var computedStyle = {};
 
  for (var p in style) {
  computedStyle[p] = style[p];
  }
 
  // Compute the size
  var canvasFontSize = parseFloat(element.currentStyle.fontSize),
  fontSize = parseFloat(style.size);
 
  if (typeof style.size == 'number') {
  computedStyle.size = style.size;
  } else if (style.size.indexOf('px') != -1) {
  computedStyle.size = fontSize;
  } else if (style.size.indexOf('em') != -1) {
  computedStyle.size = canvasFontSize * fontSize;
  } else if(style.size.indexOf('%') != -1) {
  computedStyle.size = (canvasFontSize / 100) * fontSize;
  } else if (style.size.indexOf('pt') != -1) {
  computedStyle.size = fontSize / .75;
  } else {
  computedStyle.size = canvasFontSize;
  }
 
  // Different scaling between normal text and VML text. This was found using
  // trial and error to get the same size as non VML text.
  computedStyle.size *= 0.981;
 
  return computedStyle;
  }
 
  function buildStyle(style) {
  return style.style + ' ' + style.variant + ' ' + style.weight + ' ' +
  style.size + 'px ' + style.family;
  }
 
  function processLineCap(lineCap) {
  switch (lineCap) {
  case 'butt':
  return 'flat';
  case 'round':
  return 'round';
  case 'square':
  default:
  return 'square';
  }
  }
 
  /**
  * This class implements CanvasRenderingContext2D interface as described by
  * the WHATWG.
  * @param {HTMLElement} surfaceElement The element that the 2D context should
  * be associated with
  */
  function CanvasRenderingContext2D_(surfaceElement) {
  this.m_ = createMatrixIdentity();
 
  this.mStack_ = [];
  this.aStack_ = [];
  this.currentPath_ = [];
 
  // Canvas context properties
  this.strokeStyle = '#000';
  this.fillStyle = '#000';
 
  this.lineWidth = 1;
  this.lineJoin = 'miter';
  this.lineCap = 'butt';
  this.miterLimit = Z * 1;
  this.globalAlpha = 1;
  this.font = '10px sans-serif';
  this.textAlign = 'left';
  this.textBaseline = 'alphabetic';
  this.canvas = surfaceElement;
 
  var el = surfaceElement.ownerDocument.createElement('div');
  el.style.width = surfaceElement.clientWidth + 'px';
  el.style.height = surfaceElement.clientHeight + 'px';
  el.style.overflow = 'hidden';
  el.style.position = 'absolute';
  surfaceElement.appendChild(el);
 
  this.element_ = el;
  this.arcScaleX_ = 1;
  this.arcScaleY_ = 1;
  this.lineScale_ = 1;
  }
 
  var contextPrototype = CanvasRenderingContext2D_.prototype;
  contextPrototype.clearRect = function() {
  if (this.textMeasureEl_) {
  this.textMeasureEl_.removeNode(true);
  this.textMeasureEl_ = null;
  }
  this.element_.innerHTML = '';
  };
 
  contextPrototype.beginPath = function() {
  // TODO: Branch current matrix so that save/restore has no effect
  // as per safari docs.
  this.currentPath_ = [];
  };
 
  contextPrototype.moveTo = function(aX, aY) {
  var p = this.getCoords_(aX, aY);
  this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
  this.currentX_ = p.x;
  this.currentY_ = p.y;
  };
 
  contextPrototype.lineTo = function(aX, aY) {
  var p = this.getCoords_(aX, aY);
  this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
 
  this.currentX_ = p.x;
  this.currentY_ = p.y;
  };
 
  contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
  aCP2x, aCP2y,
  aX, aY) {
  var p = this.getCoords_(aX, aY);
  var cp1 = this.getCoords_(aCP1x, aCP1y);
  var cp2 = this.getCoords_(aCP2x, aCP2y);
  bezierCurveTo(this, cp1, cp2, p);
  };
 
  // Helper function that takes the already fixed cordinates.
  function bezierCurveTo(self, cp1, cp2, p) {
  self.currentPath_.push({
  type: 'bezierCurveTo',
  cp1x: cp1.x,
  cp1y: cp1.y,
  cp2x: cp2.x,
  cp2y: cp2.y,
  x: p.x,
  y: p.y
  });
  self.currentX_ = p.x;
  self.currentY_ = p.y;
  }
 
  contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
  // the following is lifted almost directly from
  // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
 
  var cp = this.getCoords_(aCPx, aCPy);
  var p = this.getCoords_(aX, aY);
 
  var cp1 = {
  x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
  y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
  };
  var cp2 = {
  x: cp1.x + (p.x - this.currentX_) / 3.0,
  y: cp1.y + (p.y - this.currentY_) / 3.0
  };
 
  bezierCurveTo(this, cp1, cp2, p);
  };
 
  contextPrototype.arc = function(aX, aY, aRadius,
  aStartAngle, aEndAngle, aClockwise) {
  aRadius *= Z;
  var arcType = aClockwise ? 'at' : 'wa';
 
  var xStart = aX + mc(aStartAngle) * aRadius - Z2;
  var yStart = aY + ms(aStartAngle) * aRadius - Z2;
 
  var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
  var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
 
  // IE won't render arches drawn counter clockwise if xStart == xEnd.
  if (xStart == xEnd && !aClockwise) {
  xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
  // that can be represented in binary
  }
 
  var p = this.getCoords_(aX, aY);
  var pStart = this.getCoords_(xStart, yStart);
  var pEnd = this.getCoords_(xEnd, yEnd);
 
  this.currentPath_.push({type: arcType,
  x: p.x,
  y: p.y,
  radius: aRadius,
  xStart: pStart.x,
  yStart: pStart.y,
  xEnd: pEnd.x,
  yEnd: pEnd.y});
 
  };
 
  contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
  this.moveTo(aX, aY);
  this.lineTo(aX + aWidth, aY);
  this.lineTo(aX + aWidth, aY + aHeight);
  this.lineTo(aX, aY + aHeight);
  this.closePath();
  };
 
  contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
  var oldPath = this.currentPath_;
  this.beginPath();
 
  this.moveTo(aX, aY);
  this.lineTo(aX + aWidth, aY);
  this.lineTo(aX + aWidth, aY + aHeight);
  this.lineTo(aX, aY + aHeight);
  this.closePath();
  this.stroke();
 
  this.currentPath_ = oldPath;
  };
 
  contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
  var oldPath = this.currentPath_;
  this.beginPath();
 
  this.moveTo(aX, aY);
  this.lineTo(aX + aWidth, aY);
  this.lineTo(aX + aWidth, aY + aHeight);
  this.lineTo(aX, aY + aHeight);
  this.closePath();
  this.fill();
 
  this.currentPath_ = oldPath;
  };
 
  contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
  var gradient = new CanvasGradient_('gradient');
  gradient.x0_ = aX0;
  gradient.y0_ = aY0;
  gradient.x1_ = aX1;
  gradient.y1_ = aY1;
  return gradient;
  };
 
  contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
  aX1, aY1, aR1) {
  var gradient = new CanvasGradient_('gradientradial');
  gradient.x0_ = aX0;
  gradient.y0_ = aY0;
  gradient.r0_ = aR0;
  gradient.x1_ = aX1;
  gradient.y1_ = aY1;
  gradient.r1_ = aR1;
  return gradient;
  };
 
  contextPrototype.drawImage = function(image, var_args) {
  var dx, dy, dw, dh, sx, sy, sw, sh;
 
  // to find the original width we overide the width and height
  var oldRuntimeWidth = image.runtimeStyle.width;
  var oldRuntimeHeight = image.runtimeStyle.height;
  image.runtimeStyle.width = 'auto';
  image.runtimeStyle.height = 'auto';
 
  // get the original size
  var w = image.width;
  var h = image.height;
 
  // and remove overides
  image.runtimeStyle.width = oldRuntimeWidth;
  image.runtimeStyle.height = oldRuntimeHeight;
 
  if (arguments.length == 3) {
  dx = arguments[1];
  dy = arguments[2];
  sx = sy = 0;
  sw = dw = w;
  sh = dh = h;
  } else if (arguments.length == 5) {
  dx = arguments[1];
  dy = arguments[2];
  dw = arguments[3];
  dh = arguments[4];
  sx = sy = 0;
  sw = w;
  sh = h;
  } else if (arguments.length == 9) {
  sx = arguments[1];
  sy = arguments[2];
  sw = arguments[3];
  sh = arguments[4];
  dx = arguments[5];
  dy = arguments[6];
  dw = arguments[7];
  dh = arguments[8];
  } else {
  throw Error('Invalid number of arguments');
  }
 
  var d = this.getCoords_(dx, dy);
 
  var w2 = sw / 2;
  var h2 = sh / 2;
 
  var vmlStr = [];
 
  var W = 10;
  var H = 10;
 
  // For some reason that I've now forgotten, using divs didn't work
  vmlStr.push(' <g_vml_:group',
  ' coordsize="', Z * W, ',', Z * H, '"',
  ' coordorigin="0,0"' ,
  ' style="width:', W, 'px;height:', H, 'px;position:absolute;');
 
  // If filters are necessary (rotation exists), create them
  // filters are bog-slow, so only create them if abbsolutely necessary
  // The following check doesn't account for skews (which don't exist
  // in the canvas spec (yet) anyway.
 
  if (this.m_[0][0] != 1 || this.m_[0][1] ||
  this.m_[1][1] != 1 || this.m_[1][0]) {
  var filter = [];
 
  // Note the 12/21 reversal
  filter.push('M11=', this.m_[0][0], ',',
  'M12=', this.m_[1][0], ',',
  'M21=', this.m_[0][1], ',',
  'M22=', this.m_[1][1], ',',
  'Dx=', mr(d.x / Z), ',',
  'Dy=', mr(d.y / Z), '');
 
  // Bounding box calculation (need to minimize displayed area so that
  // filters don't waste time on unused pixels.
  var max = d;
  var c2 = this.getCoords_(dx + dw, dy);
  var c3 = this.getCoords_(dx, dy + dh);
  var c4 = this.getCoords_(dx + dw, dy + dh);
 
  max.x = m.max(max.x, c2.x, c3.x, c4.x);
  max.y = m.max(max.y, c2.y, c3.y, c4.y);
 
  vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),
  'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',
  filter.join(''), ", sizingmethod='clip');");
 
  } else {
  vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');
  }
 
  vmlStr.push(' ">' ,
  '<g_vml_:image src="', image.src, '"',
  ' style="width:', Z * dw, 'px;',
  ' height:', Z * dh, 'px"',
  ' cropleft="', sx / w, '"',
  ' croptop="', sy / h, '"',
  ' cropright="', (w - sx - sw) / w, '"',
  ' cropbottom="', (h - sy - sh) / h, '"',
  ' />',
  '</g_vml_:group>');
 
  this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join(''));
  };
 
  contextPrototype.stroke = function(aFill) {
  var W = 10;
  var H = 10;
  // Divide the shape into chunks if it's too long because IE has a limit
  // somewhere for how long a VML shape can be. This simple division does
  // not work with fills, only strokes, unfortunately.
  var chunkSize = 5000;
 
  var min = {x: null, y: null};
  var max = {x: null, y: null};
 
  for (var j = 0; j < this.currentPath_.length; j += chunkSize) {
  var lineStr = [];
  var lineOpen = false;
 
  lineStr.push('<g_vml_:shape',
  ' filled="', !!aFill, '"',
  ' style="position:absolute;width:', W, 'px;height:', H, 'px;"',
  ' coordorigin="0,0"',
  ' coordsize="', Z * W, ',', Z * H, '"',
  ' stroked="', !aFill, '"',
  ' path="');
 
  var newSeq = false;
 
  for (var i = j; i < Math.min(j + chunkSize, this.currentPath_.length); i++) {
  if (i % chunkSize == 0 && i > 0) { // move into position for next chunk
  lineStr.push(' m ', mr(this.currentPath_[i-1].x), ',', mr(this.currentPath_[i-1].y));
  }
 
  var p = this.currentPath_[i];
  var c;
 
  switch (p.type) {
  case 'moveTo':
  c = p;
  lineStr.push(' m ', mr(p.x), ',', mr(p.y));
  break;
  case 'lineTo':
  lineStr.push(' l ', mr(p.x), ',', mr(p.y));
  break;
  case 'close':
  lineStr.push(' x ');
  p = null;
  break;
  case 'bezierCurveTo':
  lineStr.push(' c ',
  mr(p.cp1x), ',', mr(p.cp1y), ',',
  mr(p.cp2x), ',', mr(p.cp2y), ',',
  mr(p.x), ',', mr(p.y));
  break;
  case 'at':
  case 'wa':
  lineStr.push(' ', p.type, ' ',
  mr(p.x - this.arcScaleX_ * p.radius), ',',
  mr(p.y - this.arcScaleY_ * p.radius), ' ',
  mr(p.x + this.arcScaleX_ * p.radius), ',',
  mr(p.y + this.arcScaleY_ * p.radius), ' ',
  mr(p.xStart), ',', mr(p.yStart), ' ',
  mr(p.xEnd), ',', mr(p.yEnd));
  break;
  }
 
 
  // TODO: Following is broken for curves due to
  // move to proper paths.
 
  // Figure out dimensions so we can do gradient fills
  // properly
  if (p) {
  if (min.x == null || p.x < min.x) {
  min.x = p.x;
  }
  if (max.x == null || p.x > max.x) {
  max.x = p.x;
  }
  if (min.y == null || p.y < min.y) {
  min.y = p.y;
  }
  if (max.y == null || p.y > max.y) {
  max.y = p.y;
  }
  }
  }
  lineStr.push(' ">');
 
  if (!aFill) {
  appendStroke(this, lineStr);
  } else {
  appendFill(this, lineStr, min, max);
  }
 
  lineStr.push('</g_vml_:shape>');
 
  this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
  }
  };
 
  function appendStroke(ctx, lineStr) {
  var a = processStyle(ctx.strokeStyle);
  var color = a.color;
  var opacity = a.alpha * ctx.globalAlpha;
  var lineWidth = ctx.lineScale_ * ctx.lineWidth;
 
  // VML cannot correctly render a line if the width is less than 1px.
  // In that case, we dilute the color to make the line look thinner.
  if (lineWidth < 1) {
  opacity *= lineWidth;
  }
 
  lineStr.push(
  '<g_vml_:stroke',
  ' opacity="', opacity, '"',
  ' joinstyle="', ctx.lineJoin, '"',
  ' miterlimit="', ctx.miterLimit, '"',
  ' endcap="', processLineCap(ctx.lineCap), '"',
  ' weight="', lineWidth, 'px"',
  ' color="', color, '" />'
  );
  }
 
  function appendFill(ctx, lineStr, min, max) {
  var fillStyle = ctx.fillStyle;
  var arcScaleX = ctx.arcScaleX_;
  var arcScaleY = ctx.arcScaleY_;
  var width = max.x - min.x;
  var height = max.y - min.y;
  if (fillStyle instanceof CanvasGradient_) {
  // TODO: Gradients transformed with the transformation matrix.
  var angle = 0;
  var focus = {x: 0, y: 0};
 
  // additional offset
  var shift = 0;
  // scale factor for offset
  var expansion = 1;
 
  if (fillStyle.type_ == 'gradient') {
  var x0 = fillStyle.x0_ / arcScaleX;
  var y0 = fillStyle.y0_ / arcScaleY;
  var x1 = fillStyle.x1_ / arcScaleX;
  var y1 = fillStyle.y1_ / arcScaleY;
  var p0 = ctx.getCoords_(x0, y0);
  var p1 = ctx.getCoords_(x1, y1);
  var dx = p1.x - p0.x;
  var dy = p1.y - p0.y;
  angle = Math.atan2(dx, dy) * 180 / Math.PI;
 
  // The angle should be a non-negative number.
  if (angle < 0) {
  angle += 360;
  }
 
  // Very small angles produce an unexpected result because they are
  // converted to a scientific notation string.
  if (angle < 1e-6) {
  angle = 0;
  }
  } else {
  var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_);
  focus = {
  x: (p0.x - min.x) / width,
  y: (p0.y - min.y) / height
  };
 
  width /= arcScaleX * Z;
  height /= arcScaleY * Z;
  var dimension = m.max(width, height);
  shift = 2 * fillStyle.r0_ / dimension;
  expansion = 2 * fillStyle.r1_ / dimension - shift;
  }
 
  // We need to sort the color stops in ascending order by offset,
  // otherwise IE won't interpret it correctly.
  var stops = fillStyle.colors_;
  stops.sort(function(cs1, cs2) {
  return cs1.offset - cs2.offset;
  });
 
  var length = stops.length;
  var color1 = stops[0].color;
  var color2 = stops[length - 1].color;
  var opacity1 = stops[0].alpha * ctx.globalAlpha;
  var opacity2 = stops[length - 1].alpha * ctx.globalAlpha;
 
  var colors = [];
  for (var i = 0; i < length; i++) {
  var stop = stops[i];
  colors.push(stop.offset * expansion + shift + ' ' + stop.color);
  }
 
  // When colors attribute is used, the meanings of opacity and o:opacity2
  // are reversed.
  lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',
  ' method="none" focus="100%"',
  ' color="', color1, '"',
  ' color2="', color2, '"',
  ' colors="', colors.join(','), '"',
  ' opacity="', opacity2, '"',
  ' g_o_:opacity2="', opacity1, '"',
  ' angle="', angle, '"',
  ' focusposition="', focus.x, ',', focus.y, '" />');
  } else if (fillStyle instanceof CanvasPattern_) {
  if (width && height) {
  var deltaLeft = -min.x;
  var deltaTop = -min.y;
  lineStr.push('<g_vml_:fill',
  ' position="',
  deltaLeft / width * arcScaleX * arcScaleX, ',',
  deltaTop / height * arcScaleY * arcScaleY, '"',
  ' type="tile"',
  // TODO: Figure out the correct size to fit the scale.
  //' size="', w, 'px ', h, 'px"',
  ' src="', fillStyle.src_, '" />');
  }
  } else {
  var a = processStyle(ctx.fillStyle);
  var color = a.color;
  var opacity = a.alpha * ctx.globalAlpha;
  lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
  '" />');
  }
  }
 
  contextPrototype.fill = function() {
  this.stroke(true);
  };
 
  contextPrototype.closePath = function() {
  this.currentPath_.push({type: 'close'});
  };
 
  /**
  * @private
  */
  contextPrototype.getCoords_ = function(aX, aY) {
  var m = this.m_;
  return {
  x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
  y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
  };
  };
 
  contextPrototype.save = function() {
  var o = {};
  copyState(this, o);
  this.aStack_.push(o);
  this.mStack_.push(this.m_);
  this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
  };
 
  contextPrototype.restore = function() {
  if (this.aStack_.length) {
  copyState(this.aStack_.pop(), this);
  this.m_ = this.mStack_.pop();
  }
  };
 
  function matrixIsFinite(m) {
  return isFinite(m[0][0]) && isFinite(m[0][1]) &&
  isFinite(m[1][0]) && isFinite(m[1][1]) &&
  isFinite(m[2][0]) && isFinite(m[2][1]);
  }
 
  function setM(ctx, m, updateLineScale) {
  if (!matrixIsFinite(m)) {
  return;
  }
  ctx.m_ = m;
 
  if (updateLineScale) {
  // Get the line scale.
  // Determinant of this.m_ means how much the area is enlarged by the
  // transformation. So its square root can be used as a scale factor
  // for width.
  var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
  ctx.lineScale_ = sqrt(abs(det));
  }
  }
 
  contextPrototype.translate = function(aX, aY) {
  var m1 = [
  [1, 0, 0],
  [0, 1, 0],
  [aX, aY, 1]
  ];
 
  setM(this, matrixMultiply(m1, this.m_), false);
  };
 
  contextPrototype.rotate = function(aRot) {
  var c = mc(aRot);
  var s = ms(aRot);
 
  var m1 = [
  [c, s, 0],
  [-s, c, 0],
  [0, 0, 1]
  ];
 
  setM(this, matrixMultiply(m1, this.m_), false);
  };
 
  contextPrototype.scale = function(aX, aY) {
  this.arcScaleX_ *= aX;
  this.arcScaleY_ *= aY;
  var m1 = [
  [aX, 0, 0],
  [0, aY, 0],
  [0, 0, 1]
  ];
 
  setM(this, matrixMultiply(m1, this.m_), true);
  };
 
  contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {
  var m1 = [
  [m11, m12, 0],
  [m21, m22, 0],
  [dx, dy, 1]
  ];
 
  setM(this, matrixMultiply(m1, this.m_), true);
  };
 
  contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {
  var m = [
  [m11, m12, 0],
  [m21, m22, 0],
  [dx, dy, 1]
  ];
 
  setM(this, m, true);
  };
 
  /**
  * The text drawing function.
  * The maxWidth argument isn't taken in account, since no browser supports
  * it yet.
  */
  contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) {
  var m = this.m_,
  delta = 1000,
  left = 0,
  right = delta,
  offset = {x: 0, y: 0},
  lineStr = [];
 
  var fontStyle = getComputedStyle(processFontStyle(this.font),
  this.element_);
 
  var fontStyleString = buildStyle(fontStyle);
 
  var elementStyle = this.element_.currentStyle;
  var textAlign = this.textAlign.toLowerCase();
  switch (textAlign) {
  case 'left':
  case 'center':
  case 'right':
  break;
  case 'end':
  textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left';
  break;
  case 'start':
  textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left';
  break;
  default:
  textAlign = 'left';
  }
 
  // 1.75 is an arbitrary number, as there is no info about the text baseline
  switch (this.textBaseline) {
  case 'hanging':
  case 'top':
  offset.y = fontStyle.size / 1.75;
  break;
  case 'middle':
  break;
  default:
  case null:
  case 'alphabetic':
  case 'ideographic':
  case 'bottom':
  offset.y = -fontStyle.size / 2.25;
  break;
  }
 
  switch(textAlign) {
  case 'right':
  left = delta;
  right = 0.05;
  break;
  case 'center':
  left = right = delta / 2;
  break;
  }
 
  var d = this.getCoords_(x + offset.x, y + offset.y);
 
  lineStr.push('<g_vml_:line from="', -left ,' 0" to="', right ,' 0.05" ',
  ' coordsize="100 100" coordorigin="0 0"',
  ' filled="', !stroke, '" stroked="', !!stroke,
  '" style="position:absolute;width:1px;height:1px;">');
 
  if (stroke) {
  appendStroke(this, lineStr);
  } else {
  // TODO: Fix the min and max params.
  appendFill(this, lineStr, {x: -left, y: 0},
  {x: right, y: fontStyle.size});
  }
 
  var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' +
  m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0';
 
  var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z);
 
  lineStr.push('<g_vml_:skew on="t" matrix="', skewM ,'" ',
  ' offset="', skewOffset, '" origin="', left ,' 0" />',
  '<g_vml_:path textpathok="true" />',
  '<g_vml_:textpath on="true" string="',
  encodeHtmlAttribute(text),
  '" style="v-text-align:', textAlign,
  ';font:', encodeHtmlAttribute(fontStyleString),
  '" /></g_vml_:line>');
 
  this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
  };
 
  contextPrototype.fillText = function(text, x, y, maxWidth) {
  this.drawText_(text, x, y, maxWidth, false);
  };
 
  contextPrototype.strokeText = function(text, x, y, maxWidth) {
  this.drawText_(text, x, y, maxWidth, true);
  };
 
  contextPrototype.measureText = function(text) {
  if (!this.textMeasureEl_) {
  var s = '<span style="position:absolute;' +
  'top:-20000px;left:0;padding:0;margin:0;border:none;' +
  'white-space:pre;"></span>';
  this.element_.insertAdjacentHTML('beforeEnd', s);
  this.textMeasureEl_ = this.element_.lastChild;
  }
  var doc = this.element_.ownerDocument;
  this.textMeasureEl_.innerHTML = '';
  this.textMeasureEl_.style.font = this.font;
  // Don't use innerHTML or innerText because they allow markup/whitespace.
  this.textMeasureEl_.appendChild(doc.createTextNode(text));
  return {width: this.textMeasureEl_.offsetWidth};
  };
 
  /******** STUBS ********/
  contextPrototype.clip = function() {
  // TODO: Implement
  };
 
  contextPrototype.arcTo = function() {
  // TODO: Implement
  };
 
  contextPrototype.createPattern = function(image, repetition) {
  return new CanvasPattern_(image, repetition);
  };
 
  // Gradient / Pattern Stubs
  function CanvasGradient_(aType) {
  this.type_ = aType;
  this.x0_ = 0;
  this.y0_ = 0;
  this.r0_ = 0;
  this.x1_ = 0;
  this.y1_ = 0;
  this.r1_ = 0;
  this.colors_ = [];
  }
 
  CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
  aColor = processStyle(aColor);
  this.colors_.push({offset: aOffset,
  color: aColor.color,
  alpha: aColor.alpha});
  };
 
  function CanvasPattern_(image, repetition) {
  assertImageIsValid(image);
  switch (repetition) {
  case 'repeat':
  case null:
  case '':
  this.repetition_ = 'repeat';
  break
  case 'repeat-x':
  case 'repeat-y':
  case 'no-repeat':
  this.repetition_ = repetition;
  break;
  default:
  throwException('SYNTAX_ERR');
  }
 
  this.src_ = image.src;
  this.width_ = image.width;
  this.height_ = image.height;
  }
 
  function throwException(s) {
  throw new DOMException_(s);
  }
 
  function assertImageIsValid(img) {
  if (!img || img.nodeType != 1 || img.tagName != 'IMG') {
  throwException('TYPE_MISMATCH_ERR');
  }
  if (img.readyState != 'complete') {
  throwException('INVALID_STATE_ERR');
  }
  }
 
  function DOMException_(s) {
  this.code = this[s];
  this.message = s +': DOM Exception ' + this.code;
  }
  var p = DOMException_.prototype = new Error;
  p.INDEX_SIZE_ERR = 1;
  p.DOMSTRING_SIZE_ERR = 2;
  p.HIERARCHY_REQUEST_ERR = 3;
  p.WRONG_DOCUMENT_ERR = 4;
  p.INVALID_CHARACTER_ERR = 5;
  p.NO_DATA_ALLOWED_ERR = 6;
  p.NO_MODIFICATION_ALLOWED_ERR = 7;
  p.NOT_FOUND_ERR = 8;
  p.NOT_SUPPORTED_ERR = 9;
  p.INUSE_ATTRIBUTE_ERR = 10;
  p.INVALID_STATE_ERR = 11;
  p.SYNTAX_ERR = 12;
  p.INVALID_MODIFICATION_ERR = 13;
  p.NAMESPACE_ERR = 14;
  p.INVALID_ACCESS_ERR = 15;
  p.VALIDATION_ERR = 16;
  p.TYPE_MISMATCH_ERR = 17;
 
  // set up externs
  G_vmlCanvasManager = G_vmlCanvasManager_;
  CanvasRenderingContext2D = CanvasRenderingContext2D_;
  CanvasGradient = CanvasGradient_;
  CanvasPattern = CanvasPattern_;
  DOMException = DOMException_;
  })();
 
  } // if
 
  /*! Javascript plotting library for jQuery, v. 0.7.
  *
  * Released under the MIT license by IOLA, December 2007.
  *
  */
 
  // first an inline dependency, jquery.colorhelpers.js, we inline it here
  // for convenience
 
  /* Plugin for jQuery for working with colors.
  *
  * Version 1.1.
  *
  * Inspiration from jQuery color animation plugin by John Resig.
  *
  * Released under the MIT license by Ole Laursen, October 2009.
  *
  * Examples:
  *
  * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
  * var c = $.color.extract($("#mydiv"), 'background-color');
  * console.log(c.r, c.g, c.b, c.a);
  * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
  *
  * Note that .scale() and .add() return the same modified object
  * instead of making a new one.
  *
  * V. 1.1: Fix error handling so e.g. parsing an empty string does
  * produce a color rather than just crashing.
  */
  (function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
 
  // the actual Flot code
  (function($) {
  function Plot(placeholder, data_, options_, plugins) {
  // data is on the form:
  // [ series1, series2 ... ]
  // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
  // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
 
  var series = [],
  options = {
  // the color theme used for graphs
  colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
  legend: {
  show: true,
  noColumns: 1, // number of colums in legend table
  labelFormatter: null, // fn: string -> string
  labelBoxBorderColor: "#ccc", // border color for the little label boxes
  container: null, // container (as jQuery object) to put legend in, null means default on top of graph
  position: "ne", // position of default legend container within plot
  margin: 5, // distance from grid edge to default legend container within plot
  backgroundColor: null, // null means auto-detect
  backgroundOpacity: 0.85 // set to 0 to avoid background
  },
  xaxis: {
  show: null, // null = auto-detect, true = always, false = never
  position: "bottom", // or "top"
  mode: null, // null or "time"
  color: null, // base color, labels, ticks
  tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
  transform: null, // null or f: number -> number to transform axis
  inverseTransform: null, // if transform is set, this should be the inverse function
  min: null, // min. value to show, null means set automatically
  max: null, // max. value to show, null means set automatically
  autoscaleMargin: null, // margin in % to add if auto-setting min/max
  ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
  tickFormatter: null, // fn: number -> string
  labelWidth: null, // size of tick labels in pixels
  labelHeight: null,
  reserveSpace: null, // whether to reserve space even if axis isn't shown
  tickLength: null, // size in pixels of ticks, or "full" for whole line
  alignTicksWithAxis: null, // axis number or null for no sync
 
  // mode specific options
  tickDecimals: null, // no. of decimals, null means auto
  tickSize: null, // number or [number, "unit"]
  minTickSize: null, // number or [number, "unit"]
  monthNames: null, // list of names of months
  timeformat: null, // format string to use
  twelveHourClock: false // 12 or 24 time in time mode
  },
  yaxis: {
  autoscaleMargin: 0.02,
  position: "left" // or "right"
  },
  xaxes: [],
  yaxes: [],
  series: {
  points: {
  show: false,
  radius: 3,
  lineWidth: 2, // in pixels
  fill: true,
  fillColor: "#ffffff",
  symbol: "circle" // or callback
  },
  lines: {
  // we don't put in show: false so we can see
  // whether lines were actively disabled
  lineWidth: 2, // in pixels
  fill: false,
  fillColor: null,
  steps: false
  },
  bars: {
  show: false,
  lineWidth: 2, // in pixels
  barWidth: 1, // in units of the x axis
  fill: true,
  fillColor: null,
  align: "left", // or "center"
  horizontal: false
  },
  shadowSize: 3
  },
  grid: {
  show: true,
  aboveData: false,
  color: "#545454", // primary color used for outline and labels
  backgroundColor: null, // null for transparent, else color
  borderColor: null, // set if different from the grid color
  tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
  labelMargin: 5, // in pixels
  axisMargin: 8, // in pixels
  borderWidth: 2, // in pixels
  minBorderMargin: null, // in pixels, null means taken from points radius
  markings: null, // array of ranges or fn: axes -> array of ranges
  markingsColor: "#f4f4f4",
  markingsLineWidth: 2,
  // interactive stuff
  clickable: false,
  hoverable: false,
  autoHighlight: true, // highlight in case mouse is near
  mouseActiveRadius: 10 // how far the mouse can be away to activate an item
  },
  hooks: {}
  },
  canvas = null, // the canvas for the plot itself
  overlay = null, // canvas for interactive stuff on top of plot
  eventHolder = null, // jQuery object that events should be bound to
  ctx = null, octx = null,
  xaxes = [], yaxes = [],
  plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
  canvasWidth = 0, canvasHeight = 0,
  plotWidth = 0, plotHeight = 0,
  hooks = {
  processOptions: [],
  processRawData: [],
  processDatapoints: [],
  drawSeries: [],
  draw: [],
  bindEvents: [],
  drawOverlay: [],
  shutdown: []
  },
  plot = this;
 
  // public functions
  plot.setData = setData;
  plot.setupGrid = setupGrid;
  plot.draw = draw;
  plot.getPlaceholder = function() { return placeholder; };
  plot.getCanvas = function() { return canvas; };
  plot.getPlotOffset = function() { return plotOffset; };
  plot.width = function () { return plotWidth; };
  plot.height = function () { return plotHeight; };
  plot.offset = function () {
  var o = eventHolder.offset();
  o.left += plotOffset.left;
  o.top += plotOffset.top;
  return o;
  };
  plot.getData = function () { return series; };
  plot.getAxes = function () {
  var res = {}, i;
  $.each(xaxes.concat(yaxes), function (_, axis) {
  if (axis)
  res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
  });
  return res;
  };
  plot.getXAxes = function () { return xaxes; };
  plot.getYAxes = function () { return yaxes; };
  plot.c2p = canvasToAxisCoords;
  plot.p2c = axisToCanvasCoords;
  plot.getOptions = function () { return options; };
  plot.highlight = highlight;
  plot.unhighlight = unhighlight;
  plot.triggerRedrawOverlay = triggerRedrawOverlay;
  plot.pointOffset = function(point) {
  return {
  left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
  top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
  };
  };
  plot.shutdown = shutdown;
  plot.resize = function () {
  getCanvasDimensions();
  resizeCanvas(canvas);
  resizeCanvas(overlay);
  };
 
  // public attributes
  plot.hooks = hooks;
 
  // initialize
  initPlugins(plot);
  parseOptions(options_);
  setupCanvases();
  setData(data_);
  setupGrid();
  draw();
  bindEvents();
 
 
  function executeHooks(hook, args) {
  args = [plot].concat(args);
  for (var i = 0; i < hook.length; ++i)
  hook[i].apply(this, args);
  }
 
  function initPlugins() {
  for (var i = 0; i < plugins.length; ++i) {
  var p = plugins[i];
  p.init(plot);
  if (p.options)
  $.extend(true, options, p.options);
  }
  }
 
  function parseOptions(opts) {
  var i;
 
  $.extend(true, options, opts);
 
  if (options.xaxis.color == null)
  options.xaxis.color = options.grid.color;
  if (options.yaxis.color == null)
  options.yaxis.color = options.grid.color;
 
  if (options.xaxis.tickColor == null) // backwards-compatibility
  options.xaxis.tickColor = options.grid.tickColor;
  if (options.yaxis.tickColor == null) // backwards-compatibility
  options.yaxis.tickColor = options.grid.tickColor;
 
  if (options.grid.borderColor == null)
  options.grid.borderColor = options.grid.color;
  if (options.grid.tickColor == null)
  options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
 
  // fill in defaults in axes, copy at least always the
  // first as the rest of the code assumes it'll be there
  for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
  options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
  for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
  options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
 
  // backwards compatibility, to be removed in future
  if (options.xaxis.noTicks && options.xaxis.ticks == null)
  options.xaxis.ticks = options.xaxis.noTicks;
  if (options.yaxis.noTicks && options.yaxis.ticks == null)
  options.yaxis.ticks = options.yaxis.noTicks;
  if (options.x2axis) {
  options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
  options.xaxes[1].position = "top";
  }
  if (options.y2axis) {
  options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
  options.yaxes[1].position = "right";
  }
  if (options.grid.coloredAreas)
  options.grid.markings = options.grid.coloredAreas;
  if (options.grid.coloredAreasColor)
  options.grid.markingsColor = options.grid.coloredAreasColor;
  if (options.lines)
  $.extend(true, options.series.lines, options.lines);
  if (options.points)
  $.extend(true, options.series.points, options.points);
  if (options.bars)
  $.extend(true, options.series.bars, options.bars);
  if (options.shadowSize != null)
  options.series.shadowSize = options.shadowSize;
 
  // save options on axes for future reference
  for (i = 0; i < options.xaxes.length; ++i)
  getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
  for (i = 0; i < options.yaxes.length; ++i)
  getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
 
  // add hooks from options
  for (var n in hooks)
  if (options.hooks[n] && options.hooks[n].length)
  hooks[n] = hooks[n].concat(options.hooks[n]);
 
  executeHooks(hooks.processOptions, [options]);
  }
 
  function setData(d) {
  series = parseData(d);
  fillInSeriesOptions();
  processData();
  }
 
  function parseData(d) {
  var res = [];
  for (var i = 0; i < d.length; ++i) {
  var s = $.extend(true, {}, options.series);
 
  if (d[i].data != null) {
  s.data = d[i].data; // move the data instead of deep-copy
  delete d[i].data;
 
  $.extend(true, s, d[i]);
 
  d[i].data = s.data;
  }
  else
  s.data = d[i];
  res.push(s);
  }
 
  return res;
  }
 
  function axisNumber(obj, coord) {
  var a = obj[coord + "axis"];
  if (typeof a == "object") // if we got a real axis, extract number
  a = a.n;
  if (typeof a != "number")
  a = 1; // default to first axis
  return a;
  }
 
  function allAxes() {
  // return flat array without annoying null entries
  return $.grep(xaxes.concat(yaxes), function (a) { return a; });
  }
 
  function canvasToAxisCoords(pos) {
  // return an object with x/y corresponding to all used axes
  var res = {}, i, axis;
  for (i = 0; i < xaxes.length; ++i) {
  axis = xaxes[i];
  if (axis && axis.used)
  res["x" + axis.n] = axis.c2p(pos.left);
  }
 
  for (i = 0; i < yaxes.length; ++i) {
  axis = yaxes[i];
  if (axis && axis.used)
  res["y" + axis.n] = axis.c2p(pos.top);
  }
 
  if (res.x1 !== undefined)
  res.x = res.x1;
  if (res.y1 !== undefined)
  res.y = res.y1;
 
  return res;
  }
 
  function axisToCanvasCoords(pos) {
  // get canvas coords from the first pair of x/y found in pos
  var res = {}, i, axis, key;
 
  for (i = 0; i < xaxes.length; ++i) {
  axis = xaxes[i];
  if (axis && axis.used) {
  key = "x" + axis.n;
  if (pos[key] == null && axis.n == 1)
  key = "x";
 
  if (pos[key] != null) {
  res.left = axis.p2c(pos[key]);
  break;
  }
  }
  }
 
  for (i = 0; i < yaxes.length; ++i) {
  axis = yaxes[i];
  if (axis && axis.used) {
  key = "y" + axis.n;
  if (pos[key] == null && axis.n == 1)
  key = "y";
 
  if (pos[key] != null) {
  res.top = axis.p2c(pos[key]);
  break;
  }
  }
  }
 
  return res;
  }
 
  function getOrCreateAxis(axes, number) {
  if (!axes[number - 1])
  axes[number - 1] = {
  n: number, // save the number for future reference
  direction: axes == xaxes ? "x" : "y",
  options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
  };
 
  return axes[number - 1];
  }
 
  function fillInSeriesOptions() {
  var i;
 
  // collect what we already got of colors
  var neededColors = series.length,
  usedColors = [],
  assignedColors = [];
  for (i = 0; i < series.length; ++i) {
  var sc = series[i].color;
  if (sc != null) {
  --neededColors;
  if (typeof sc == "number")
  assignedColors.push(sc);
  else
  usedColors.push($.color.parse(series[i].color));
  }
  }
 
  // we might need to generate more colors if higher indices
  // are assigned
  for (i = 0; i < assignedColors.length; ++i) {
  neededColors = Math.max(neededColors, assignedColors[i] + 1);
  }
 
  // produce colors as needed
  var colors = [], variation = 0;
  i = 0;
  while (colors.length < neededColors) {
  var c;
  if (options.colors.length == i) // check degenerate case
  c = $.color.make(100, 100, 100);
  else
  c = $.color.parse(options.colors[i]);
 
  // vary color if needed
  var sign = variation % 2 == 1 ? -1 : 1;
  c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
 
  // FIXME: if we're getting to close to something else,
  // we should probably skip this one
  colors.push(c);
 
  ++i;
  if (i >= options.colors.length) {
  i = 0;
  ++variation;
  }
  }
 
  // fill in the options
  var colori = 0, s;
  for (i = 0; i < series.length; ++i) {
  s = series[i];
 
  // assign colors
  if (s.color == null) {
  s.color = colors[colori].toString();
  ++colori;
  }
  else if (typeof s.color == "number")
  s.color = colors[s.color].toString();
 
  // turn on lines automatically in case nothing is set
  if (s.lines.show == null) {
  var v, show = true;
  for (v in s)
  if (s[v] && s[v].show) {
  show = false;
  break;
  }
  if (show)
  s.lines.show = true;
  }
 
  // setup axes
  s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
  s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
  }
  }
 
  function processData() {
  var topSentry = Number.POSITIVE_INFINITY,
  bottomSentry = Number.NEGATIVE_INFINITY,
  fakeInfinity = Number.MAX_VALUE,
  i, j, k, m, length,
  s, points, ps, x, y, axis, val, f, p;
 
  function updateAxis(axis, min, max) {
  if (min < axis.datamin && min != -fakeInfinity)
  axis.datamin = min;
  if (max > axis.datamax && max != fakeInfinity)
  axis.datamax = max;
  }
 
  $.each(allAxes(), function (_, axis) {
  // init axis
  axis.datamin = topSentry;
  axis.datamax = bottomSentry;
  axis.used = false;
  });
 
  for (i = 0; i < series.length; ++i) {
  s = series[i];
  s.datapoints = { points: [] };
 
  executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
  }
 
  // first pass: clean and copy data
  for (i = 0; i < series.length; ++i) {
  s = series[i];
 
  var data = s.data, format = s.datapoints.format;
 
  if (!format) {
  format = [];
  // find out how to copy
  format.push({ x: true, number: true, required: true });
  format.push({ y: true, number: true, required: true });
 
  if (s.bars.show || (s.lines.show && s.lines.fill)) {
  format.push({ y: true, number: true, required: false, defaultValue: 0 });
  if (s.bars.horizontal) {
  delete format[format.length - 1].y;
  format[format.length - 1].x = true;
  }
  }
 
  s.datapoints.format = format;
  }
 
  if (s.datapoints.pointsize != null)
  continue; // already filled in
 
  s.datapoints.pointsize = format.length;
 
  ps = s.datapoints.pointsize;
  points = s.datapoints.points;
 
  insertSteps = s.lines.show && s.lines.steps;
  s.xaxis.used = s.yaxis.used = true;
 
  for (j = k = 0; j < data.length; ++j, k += ps) {
  p = data[j];
 
  var nullify = p == null;
  if (!nullify) {
  for (m = 0; m < ps; ++m) {
  val = p[m];
  f = format[m];
 
  if (f) {
  if (f.number && val != null) {
  val = +val; // convert to number
  if (isNaN(val))
  val = null;
  else if (val == Infinity)
  val = fakeInfinity;
  else if (val == -Infinity)
  val = -fakeInfinity;
  }
 
  if (val == null) {
  if (f.required)
  nullify = true;
 
  if (f.defaultValue != null)
  val = f.defaultValue;
  }
  }
 
  points[k + m] = val;
  }
  }
 
  if (nullify) {
  for (m = 0; m < ps; ++m) {
  val = points[k + m];
  if (val != null) {
  f = format[m];
  // extract min/max info
  if (f.x)
  updateAxis(s.xaxis, val, val);
  if (f.y)
  updateAxis(s.yaxis, val, val);
  }
  points[k + m] = null;
  }
  }
  else {
  // a little bit of line specific stuff that
  // perhaps shouldn't be here, but lacking
  // better means...
  if (insertSteps && k > 0
  && points[k - ps] != null
  && points[k - ps] != points[k]
  && points[k - ps + 1] != points[k + 1]) {
  // copy the point to make room for a middle point
  for (m = 0; m < ps; ++m)
  points[k + ps + m] = points[k + m];
 
  // middle point has same y
  points[k + 1] = points[k - ps + 1];
 
  // we've added a point, better reflect that
  k += ps;
  }
  }
  }
  }
 
  // give the hooks a chance to run
  for (i = 0; i < series.length; ++i) {
  s = series[i];
 
  executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
  }
 
  // second pass: find datamax/datamin for auto-scaling
  for (i = 0; i < series.length; ++i) {
  s = series[i];
  points = s.datapoints.points,
  ps = s.datapoints.pointsize;
 
  var xmin = topSentry, ymin = topSentry,
  xmax = bottomSentry, ymax = bottomSentry;
 
  for (j = 0; j < points.length; j += ps) {
  if (points[j] == null)
  continue;
 
  for (m = 0; m < ps; ++m) {
  val = points[j + m];
  f = format[m];
  if (!f || val == fakeInfinity || val == -fakeInfinity)
  continue;
 
  if (f.x) {
  if (val < xmin)
  xmin = val;
  if (val > xmax)
  xmax = val;
  }
  if (f.y) {
  if (val < ymin)
  ymin = val;
  if (val > ymax)
  ymax = val;
  }
  }
  }
 
  if (s.bars.show) {
  // make sure we got room for the bar on the dancing floor
  var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
  if (s.bars.horizontal) {
  ymin += delta;
  ymax += delta + s.bars.barWidth;
  }
  else {
  xmin += delta;
  xmax += delta + s.bars.barWidth;
  }
  }
 
  updateAxis(s.xaxis, xmin, xmax);
  updateAxis(s.yaxis, ymin, ymax);
  }
 
  $.each(allAxes(), function (_, axis) {
  if (axis.datamin == topSentry)
  axis.datamin = null;
  if (axis.datamax == bottomSentry)
  axis.datamax = null;
  });
  }
 
  function makeCanvas(skipPositioning, cls) {
  var c = document.createElement('canvas');
  c.className = cls;
  c.width = canvasWidth;
  c.height = canvasHeight;
 
  if (!skipPositioning)
  $(c).css({ position: 'absolute', left: 0, top: 0 });
 
  $(c).appendTo(placeholder);
 
  if (!c.getContext) // excanvas hack
  c = window.G_vmlCanvasManager.initElement(c);
 
  // used for resetting in case we get replotted
  c.getContext("2d").save();
 
  return c;
  }
 
  function getCanvasDimensions() {
  canvasWidth = placeholder.width();
  canvasHeight = placeholder.height();
 
  if (canvasWidth <= 0 || canvasHeight <= 0)
  throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
  }
 
  function resizeCanvas(c) {
  // resizing should reset the state (excanvas seems to be
  // buggy though)
  if (c.width != canvasWidth)
  c.width = canvasWidth;
 
  if (c.height != canvasHeight)
  c.height = canvasHeight;
 
  // so try to get back to the initial state (even if it's
  // gone now, this should be safe according to the spec)
  var cctx = c.getContext("2d");
  cctx.restore();
 
  // and save again
  cctx.save();
  }
 
  function setupCanvases() {
  var reused,
  existingCanvas = placeholder.children("canvas.base"),
  existingOverlay = placeholder.children("canvas.overlay");
 
  if (existingCanvas.length == 0 || existingOverlay == 0) {
  // init everything
 
  placeholder.html(""); // make sure placeholder is clear
 
  placeholder.css({ padding: 0 }); // padding messes up the positioning
 
  if (placeholder.css("position") == 'static')
  placeholder.css("position", "relative"); // for positioning labels and overlay
 
  getCanvasDimensions();
 
  canvas = makeCanvas(true, "base");
  overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
 
  reused = false;
  }
  else {
  // reuse existing elements
 
  canvas = existingCanvas.get(0);
  overlay = existingOverlay.get(0);
 
  reused = true;
  }
 
  ctx = canvas.getContext("2d");
  octx = overlay.getContext("2d");
 
  // we include the canvas in the event holder too, because IE 7
  // sometimes has trouble with the stacking order
  eventHolder = $([overlay, canvas]);
 
  if (reused) {
  // run shutdown in the old plot object
  placeholder.data("plot").shutdown();
 
  // reset reused canvases
  plot.resize();
 
  // make sure overlay pixels are cleared (canvas is cleared when we redraw)
  octx.clearRect(0, 0, canvasWidth, canvasHeight);
 
  // then whack any remaining obvious garbage left
  eventHolder.unbind();
  placeholder.children().not([canvas, overlay]).remove();
  }
 
  // save in case we get replotted
  placeholder.data("plot", plot);
  }
 
  function bindEvents() {
  // bind events
  if (options.grid.hoverable) {
  eventHolder.mousemove(onMouseMove);
  eventHolder.mouseleave(onMouseLeave);
  }
 
  if (options.grid.clickable)
  eventHolder.click(onClick);
 
  executeHooks(hooks.bindEvents, [eventHolder]);
  }
 
  function shutdown() {
  if (redrawTimeout)
  clearTimeout(redrawTimeout);
 
  eventHolder.unbind("mousemove", onMouseMove);
  eventHolder.unbind("mouseleave", onMouseLeave);
  eventHolder.unbind("click", onClick);
 
  executeHooks(hooks.shutdown, [eventHolder]);
  }
 
  function setTransformationHelpers(axis) {
  // set helper functions on the axis, assumes plot area
  // has been computed already
 
  function identity(x) { return x; }
 
  var s, m, t = axis.options.transform || identity,
  it = axis.options.inverseTransform;
 
  // precompute how much the axis is scaling a point
  // in canvas space
  if (axis.direction == "x") {
  s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
  m = Math.min(t(axis.max), t(axis.min));
  }
  else {
  s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
  s = -s;
  m = Math.max(t(axis.max), t(axis.min));
  }
 
  // data point to canvas coordinate
  if (t == identity) // slight optimization
  axis.p2c = function (p) { return (p - m) * s; };
  else
  axis.p2c = function (p) { return (t(p) - m) * s; };
  // canvas coordinate to data point
  if (!it)
  axis.c2p = function (c) { return m + c / s; };
  else
  axis.c2p = function (c) { return it(m + c / s); };
  }
 
  function measureTickLabels(axis) {
  var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
  l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
 
  function makeDummyDiv(labels, width) {
  return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +
  '<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'
  + labels.join("") + '</div></div>')
  .appendTo(placeholder);
  }
 
  if (axis.direction == "x") {
  // to avoid measuring the widths of the labels (it's slow), we
  // construct fixed-size boxes and put the labels inside
  // them, we don't need the exact figures and the
  // fixed-size box content is easy to center
  if (w == null)
  w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
 
  // measure x label heights
  if (h == null) {
  labels = [];
  for (i = 0; i < ticks.length; ++i) {
  l = ticks[i].label;
  if (l)
  labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');
  }
 
  if (labels.length > 0) {
  // stick them all in the same div and measure
  // collective height
  labels.push('<div style="clear:left"></div>');
  dummyDiv = makeDummyDiv(labels, "width:10000px;");
  h = dummyDiv.height();
  dummyDiv.remove();
  }
  }
  }
  else if (w == null || h == null) {
  // calculate y label dimensions
  for (i = 0; i < ticks.length; ++i) {
  l = ticks[i].label;
  if (l)
  labels.push('<div class="tickLabel">' + l + '</div>');
  }
 
  if (labels.length > 0) {
  dummyDiv = makeDummyDiv(labels, "");
  if (w == null)
  w = dummyDiv.children().width();
  if (h == null)
  h = dummyDiv.find("div.tickLabel").height();
  dummyDiv.remove();
  }
  }
 
  if (w == null)
  w = 0;
  if (h == null)
  h = 0;
 
  axis.labelWidth = w;
  axis.labelHeight = h;
  }
 
  function allocateAxisBoxFirstPhase(axis) {
  // find the bounding box of the axis by looking at label
  // widths/heights and ticks, make room by diminishing the
  // plotOffset
 
  var lw = axis.labelWidth,
  lh = axis.labelHeight,
  pos = axis.options.position,
  tickLength = axis.options.tickLength,
  axismargin = options.grid.axisMargin,
  padding = options.grid.labelMargin,
  all = axis.direction == "x" ? xaxes : yaxes,
  index;
 
  // determine axis margin
  var samePosition = $.grep(all, function (a) {
  return a && a.options.position == pos && a.reserveSpace;
  });
  if ($.inArray(axis, samePosition) == samePosition.length - 1)
  axismargin = 0; // outermost
 
  // determine tick length - if we're innermost, we can use "full"
  if (tickLength == null)
  tickLength = "full";
 
  var sameDirection = $.grep(all, function (a) {
  return a && a.reserveSpace;
  });
 
  var innermost = $.inArray(axis, sameDirection) == 0;
  if (!innermost && tickLength == "full")
  tickLength = 5;
 
  if (!isNaN(+tickLength))
  padding += +tickLength;
 
  // compute box
  if (axis.direction == "x") {
  lh += padding;
 
  if (pos == "bottom") {
  plotOffset.bottom += lh + axismargin;
  axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
  }
  else {
  axis.box = { top: plotOffset.top + axismargin, height: lh };
  plotOffset.top += lh + axismargin;
  }
  }
  else {
  lw += padding;
 
  if (pos == "left") {
  axis.box = { left: plotOffset.left + axismargin, width: lw };
  plotOffset.left += lw + axismargin;
  }
  else {
  plotOffset.right += lw + axismargin;
  axis.box = { left: canvasWidth - plotOffset.right, width: lw };
  }
  }
 
  // save for future reference
  axis.position = pos;
  axis.tickLength = tickLength;
  axis.box.padding = padding;
  axis.innermost = innermost;
  }
 
  function allocateAxisBoxSecondPhase(axis) {
  // set remaining bounding box coordinates
  if (axis.direction == "x") {
  axis.box.left = plotOffset.left;
  axis.box.width = plotWidth;
  }
  else {
  axis.box.top = plotOffset.top;
  axis.box.height = plotHeight;
  }
  }
 
  function setupGrid() {
  var i, axes = allAxes();
 
  // first calculate the plot and axis box dimensions
 
  $.each(axes, function (_, axis) {
  axis.show = axis.options.show;
  if (axis.show == null)
  axis.show = axis.used; // by default an axis is visible if it's got data
 
  axis.reserveSpace = axis.show || axis.options.reserveSpace;
 
  setRange(axis);
  });
 
  allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
 
  plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
  if (options.grid.show) {
  $.each(allocatedAxes, function (_, axis) {
  // make the ticks
  setupTickGeneration(axis);
  setTicks(axis);
  snapRangeToTicks(axis, axis.ticks);
 
  // find labelWidth/Height for axis
  measureTickLabels(axis);
  });
 
  // with all dimensions in house, we can compute the
  // axis boxes, start from the outside (reverse order)
  for (i = allocatedAxes.length - 1; i >= 0; --i)
  allocateAxisBoxFirstPhase(allocatedAxes[i]);
 
  // make sure we've got enough space for things that
  // might stick out
  var minMargin = options.grid.minBorderMargin;
  if (minMargin == null) {
  minMargin = 0;
  for (i = 0; i < series.length; ++i)
  minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
  }
 
  for (var a in plotOffset) {
  plotOffset[a] += options.grid.borderWidth;
  plotOffset[a] = Math.max(minMargin, plotOffset[a]);
  }
  }
 
  plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
  plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
 
  // now we got the proper plotWidth/Height, we can compute the scaling
  $.each(axes, function (_, axis) {
  setTransformationHelpers(axis);
  });
 
  if (options.grid.show) {
  $.each(allocatedAxes, function (_, axis) {
  allocateAxisBoxSecondPhase(axis);
  });
 
  insertAxisLabels();
  }
 
  insertLegend();
  }
 
  function setRange(axis) {
  var opts = axis.options,
  min = +(opts.min != null ? opts.min : axis.datamin),
  max = +(opts.max != null ? opts.max : axis.datamax),
  delta = max - min;
 
  if (delta == 0.0) {
  // degenerate case
  var widen = max == 0 ? 1 : 0.01;
 
  if (opts.min == null)
  min -= widen;
  // always widen max if we couldn't widen min to ensure we
  // don't fall into min == max which doesn't work
  if (opts.max == null || opts.min != null)
  max += widen;
  }
  else {
  // consider autoscaling
  var margin = opts.autoscaleMargin;
  if (margin != null) {
  if (opts.min == null) {
  min -= delta * margin;
  // make sure we don't go below zero if all values
  // are positive
  if (min < 0 && axis.datamin != null && axis.datamin >= 0)
  min = 0;
  }
  if (opts.max == null) {
  max += delta * margin;
  if (max > 0 && axis.datamax != null && axis.datamax <= 0)
  max = 0;
  }
  }
  }
  axis.min = min;
  axis.max = max;
  }
 
  function setupTickGeneration(axis) {
  var opts = axis.options;
 
  // estimate number of ticks
  var noTicks;
  if (typeof opts.ticks == "number" && opts.ticks > 0)
  noTicks = opts.ticks;
  else
  // heuristic based on the model a*sqrt(x) fitted to
  // some data points that seemed reasonable
  noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
 
  var delta = (axis.max - axis.min) / noTicks,
  size, generator, unit, formatter, i, magn, norm;
 
  if (opts.mode == "time") {
  // pretty handling of time
 
  // map of app. size of time units in milliseconds
  var timeUnitSize = {
  "second": 1000,
  "minute": 60 * 1000,
  "hour": 60 * 60 * 1000,
  "day": 24 * 60 * 60 * 1000,
  "month": 30 * 24 * 60 * 60 * 1000,
  "year": 365.2425 * 24 * 60 * 60 * 1000
  };
 
 
  // the allowed tick sizes, after 1 year we use
  // an integer algorithm
  var spec = [
  [1, "second"], [2, "second"], [5, "second"], [10, "second"],
  [30, "second"],
  [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
  [30, "minute"],
  [1, "hour"], [2, "hour"], [4, "hour"],
  [8, "hour"], [12, "hour"],
  [1, "day"], [2, "day"], [3, "day"],
  [0.25, "month"], [0.5, "month"], [1, "month"],
  [2, "month"], [3, "month"], [6, "month"],
  [1, "year"]
  ];
 
  var minSize = 0;
  if (opts.minTickSize != null) {
  if (typeof opts.tickSize == "number")
  minSize = opts.tickSize;
  else
  minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
  }
 
  for (var i = 0; i < spec.length - 1; ++i)
  if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
  + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
  && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
  break;
  size = spec[i][0];
  unit = spec[i][1];
 
  // special-case the possibility of several years
  if (unit == "year") {
  magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
  norm = (delta / timeUnitSize.year) / magn;
  if (norm < 1.5)
  size = 1;
  else if (norm < 3)
  size = 2;
  else if (norm < 7.5)
  size = 5;
  else
  size = 10;
 
  size *= magn;
  }
 
  axis.tickSize = opts.tickSize || [size, unit];
 
  generator = function(axis) {
  var ticks = [],
  tickSize = axis.tickSize[0], unit = axis.tickSize[1],
  d = new Date(axis.min);
 
  var step = tickSize * timeUnitSize[unit];
 
  if (unit == "second")
  d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
  if (unit == "minute")
  d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
  if (unit == "hour")
  d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
  if (unit == "month")
  d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
  if (unit == "year")
  d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
 
  // reset smaller components
  d.setUTCMilliseconds(0);
  if (step >= timeUnitSize.minute)
  d.setUTCSeconds(0);
  if (step >= timeUnitSize.hour)
  d.setUTCMinutes(0);
  if (step >= timeUnitSize.day)
  d.setUTCHours(0);
  if (step >= timeUnitSize.day * 4)
  d.setUTCDate(1);
  if (step >= timeUnitSize.year)
  d.setUTCMonth(0);
 
 
  var carry = 0, v = Number.NaN, prev;
  do {
  prev = v;
  v = d.getTime();
  ticks.push(v);
  if (unit == "month") {
  if (tickSize < 1) {
  // a bit complicated - we'll divide the month
  // up but we need to take care of fractions
  // so we don't end up in the middle of a day
  d.setUTCDate(1);
  var start = d.getTime();
  d.setUTCMonth(d.getUTCMonth() + 1);
  var end = d.getTime();
  d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
  carry = d.getUTCHours();
  d.setUTCHours(0);
  }
  else
  d.setUTCMonth(d.getUTCMonth() + tickSize);
  }
  else if (unit == "year") {
  d.setUTCFullYear(d.getUTCFullYear() + tickSize);
  }
  else
  d.setTime(v + step);
  } while (v < axis.max && v != prev);
 
  return ticks;
  };
 
  formatter = function (v, axis) {
  var d = new Date(v);
 
  // first check global format
  if (opts.timeformat != null)
  return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
 
  var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
  var span = axis.max - axis.min;
  var suffix = (opts.twelveHourClock) ? " %p" : "";
 
  if (t < timeUnitSize.minute)
  fmt = "%h:%M:%S" + suffix;
  else if (t < timeUnitSize.day) {
  if (span < 2 * timeUnitSize.day)
  fmt = "%h:%M" + suffix;
  else
  fmt = "%b %d %h:%M" + suffix;
  }
  else if (t < timeUnitSize.month)
  fmt = "%b %d";
  else if (t < timeUnitSize.year) {
  if (span < timeUnitSize.year)
  fmt = "%b";
  else
  fmt = "%b %y";
  }
  else
  fmt = "%y";
 
  return $.plot.formatDate(d, fmt, opts.monthNames);
  };
  }
  else {
  // pretty rounding of base-10 numbers
  var maxDec = opts.tickDecimals;
  var dec = -Math.floor(Math.log(delta) / Math.LN10);
  if (maxDec != null && dec > maxDec)
  dec = maxDec;
 
  magn = Math.pow(10, -dec);
  norm = delta / magn; // norm is between 1.0 and 10.0
 
  if (norm < 1.5)
  size = 1;
  else if (norm < 3) {
  size = 2;
  // special case for 2.5, requires an extra decimal
  if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
  size = 2.5;
  ++dec;
  }
  }
  else if (norm < 7.5)
  size = 5;
  else
  size = 10;
 
  size *= magn;
 
  if (opts.minTickSize != null && size < opts.minTickSize)
  size = opts.minTickSize;
 
  axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
  axis.tickSize = opts.tickSize || size;
 
  generator = function (axis) {
  var ticks = [];
 
  // spew out all possible ticks
  var start = floorInBase(axis.min, axis.tickSize),
  i = 0, v = Number.NaN, prev;
  do {
  prev = v;
  v = start + i * axis.tickSize;
  ticks.push(v);
  ++i;
  } while (v < axis.max && v != prev);
  return ticks;
  };
 
  formatter = function (v, axis) {
  return v.toFixed(axis.tickDecimals);
  };
  }
 
  if (opts.alignTicksWithAxis != null) {
  var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
  if (otherAxis && otherAxis.used && otherAxis != axis) {
  // consider snapping min/max to outermost nice ticks
  var niceTicks = generator(axis);
  if (niceTicks.length > 0) {
  if (opts.min == null)
  axis.min = Math.min(axis.min, niceTicks[0]);
  if (opts.max == null && niceTicks.length > 1)
  axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
  }
 
  generator = function (axis) {
  // copy ticks, scaled to this axis
  var ticks = [], v, i;
  for (i = 0; i < otherAxis.ticks.length; ++i) {
  v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
  v = axis.min + v * (axis.max - axis.min);
  ticks.push(v);
  }
  return ticks;
  };
 
  // we might need an extra decimal since forced
  // ticks don't necessarily fit naturally
  if (axis.mode != "time" && opts.tickDecimals == null) {
  var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
  ts = generator(axis);
 
  // only proceed if the tick interval rounded
  // with an extra decimal doesn't give us a
  // zero at end
  if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
  axis.tickDecimals = extraDec;
  }
  }
  }
 
  axis.tickGenerator = generator;
  if ($.isFunction(opts.tickFormatter))
  axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
  else
  axis.tickFormatter = formatter;
  }
 
  function setTicks(axis) {
  var oticks = axis.options.ticks, ticks = [];
  if (oticks == null || (typeof oticks == "number" && oticks > 0))
  ticks = axis.tickGenerator(axis);
  else if (oticks) {
  if ($.isFunction(oticks))
  // generate the ticks
  ticks = oticks({ min: axis.min, max: axis.max });
  else
  ticks = oticks;
  }
 
  // clean up/labelify the supplied ticks, copy them over
  var i, v;
  axis.ticks = [];
  for (i = 0; i < ticks.length; ++i) {
  var label = null;
  var t = ticks[i];
  if (typeof t == "object") {
  v = +t[0];
  if (t.length > 1)
  label = t[1];
  }
  else
  v = +t;
  if (label == null)
  label = axis.tickFormatter(v, axis);
  if (!isNaN(v))
  axis.ticks.push({ v: v, label: label });
  }
  }
 
  function snapRangeToTicks(axis, ticks) {
  if (axis.options.autoscaleMargin && ticks.length > 0) {
  // snap to ticks
  if (axis.options.min == null)
  axis.min = Math.min(axis.min, ticks[0].v);
  if (axis.options.max == null && ticks.length > 1)
  axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
  }
  }
 
  function draw() {
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
 
  var grid = options.grid;
 
  // draw background, if any
  if (grid.show && grid.backgroundColor)
  drawBackground();
 
  if (grid.show && !grid.aboveData)
  drawGrid();
 
  for (var i = 0; i < series.length; ++i) {
  executeHooks(hooks.drawSeries, [ctx, series[i]]);
  drawSeries(series[i]);
  }
 
  executeHooks(hooks.draw, [ctx]);
 
  if (grid.show && grid.aboveData)
  drawGrid();
  }
 
  function extractRange(ranges, coord) {
  var axis, from, to, key, axes = allAxes();
 
  for (i = 0; i < axes.length; ++i) {
  axis = axes[i];
  if (axis.direction == coord) {
  key = coord + axis.n + "axis";
  if (!ranges[key] && axis.n == 1)
  key = coord + "axis"; // support x1axis as xaxis
  if (ranges[key]) {
  from = ranges[key].from;
  to = ranges[key].to;
  break;
  }
  }
  }
 
  // backwards-compat stuff - to be removed in future
  if (!ranges[key]) {
  axis = coord == "x" ? xaxes[0] : yaxes[0];
  from = ranges[coord + "1"];
  to = ranges[coord + "2"];
  }
 
  // auto-reverse as an added bonus
  if (from != null && to != null && from > to) {
  var tmp = from;
  from = to;
  to = tmp;
  }
 
  return { from: from, to: to, axis: axis };
  }
 
  function drawBackground() {
  ctx.save();
  ctx.translate(plotOffset.left, plotOffset.top);
 
  ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
  ctx.fillRect(0, 0, plotWidth, plotHeight);
  ctx.restore();
  }
 
  function drawGrid() {
  var i;
 
  ctx.save();
  ctx.translate(plotOffset.left, plotOffset.top);
 
  // draw markings
  var markings = options.grid.markings;
  if (markings) {
  if ($.isFunction(markings)) {
  var axes = plot.getAxes();
  // xmin etc. is backwards compatibility, to be
  // removed in the future
  axes.xmin = axes.xaxis.min;
  axes.xmax = axes.xaxis.max;
  axes.ymin = axes.yaxis.min;
  axes.ymax = axes.yaxis.max;
 
  markings = markings(axes);
  }
 
  for (i = 0; i < markings.length; ++i) {
  var m = markings[i],
  xrange = extractRange(m, "x"),
  yrange = extractRange(m, "y");
 
  // fill in missing
  if (xrange.from == null)
  xrange.from = xrange.axis.min;
  if (xrange.to == null)
  xrange.to = xrange.axis.max;
  if (yrange.from == null)
  yrange.from = yrange.axis.min;
  if (yrange.to == null)
  yrange.to = yrange.axis.max;
 
  // clip
  if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
  yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
  continue;
 
  xrange.from = Math.max(xrange.from, xrange.axis.min);
  xrange.to = Math.min(xrange.to, xrange.axis.max);
  yrange.from = Math.max(yrange.from, yrange.axis.min);
  yrange.to = Math.min(yrange.to, yrange.axis.max);
 
  if (xrange.from == xrange.to && yrange.from == yrange.to)
  continue;
 
  // then draw
  xrange.from = xrange.axis.p2c(xrange.from);
  xrange.to = xrange.axis.p2c(xrange.to);
  yrange.from = yrange.axis.p2c(yrange.from);
  yrange.to = yrange.axis.p2c(yrange.to);
 
  if (xrange.from == xrange.to || yrange.from == yrange.to) {
  // draw line
  ctx.beginPath();
  ctx.strokeStyle = m.color || options.grid.markingsColor;
  ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
  ctx.moveTo(xrange.from, yrange.from);
  ctx.lineTo(xrange.to, yrange.to);
  ctx.stroke();
  }
  else {
  // fill area
  ctx.fillStyle = m.color || options.grid.markingsColor;
  ctx.fillRect(xrange.from, yrange.to,
  xrange.to - xrange.from,
  yrange.from - yrange.to);
  }
  }
  }
 
  // draw the ticks
  var axes = allAxes(), bw = options.grid.borderWidth;
 
  for (var j = 0; j < axes.length; ++j) {
  var axis = axes[j], box = axis.box,
  t = axis.tickLength, x, y, xoff, yoff;
  if (!axis.show || axis.ticks.length == 0)
  continue
 
  ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
  ctx.lineWidth = 1;
 
  // find the edges
  if (axis.direction == "x") {
  x = 0;
  if (t == "full")
  y = (axis.position == "top" ? 0 : plotHeight);
  else
  y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
  }
  else {
  y = 0;
  if (t == "full")
  x = (axis.position == "left" ? 0 : plotWidth);
  else
  x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
  }
 
  // draw tick bar
  if (!axis.innermost) {
  ctx.beginPath();
  xoff = yoff = 0;
  if (axis.direction == "x")
  xoff = plotWidth;
  else
  yoff = plotHeight;
 
  if (ctx.lineWidth == 1) {
  x = Math.floor(x) + 0.5;
  y = Math.floor(y) + 0.5;
  }
 
  ctx.moveTo(x, y);
  ctx.lineTo(x + xoff, y + yoff);
  ctx.stroke();
  }
 
  // draw ticks
  ctx.beginPath();
  for (i = 0; i < axis.ticks.length; ++i) {
  var v = axis.ticks[i].v;
 
  xoff = yoff = 0;
 
  if (v < axis.min || v > axis.max
  // skip those lying on the axes if we got a border
  || (t == "full" && bw > 0
  && (v == axis.min || v == axis.max)))
  continue;
 
  if (axis.direction == "x") {
  x = axis.p2c(v);
  yoff = t == "full" ? -plotHeight : t;
 
  if (axis.position == "top")
  yoff = -yoff;
  }
  else {
  y = axis.p2c(v);
  xoff = t == "full" ? -plotWidth : t;
 
  if (axis.position == "left")
  xoff = -xoff;
  }
 
  if (ctx.lineWidth == 1) {
  if (axis.direction == "x")
  x = Math.floor(x) + 0.5;
  else
  y = Math.floor(y) + 0.5;
  }
 
  ctx.moveTo(x, y);
  ctx.lineTo(x + xoff, y + yoff);
  }
 
  ctx.stroke();
  }
 
 
  // draw border
  if (bw) {
  ctx.lineWidth = bw;
  ctx.strokeStyle = options.grid.borderColor;
  ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
  }
 
  ctx.restore();
  }
 
  function insertAxisLabels() {
  placeholder.find(".tickLabels").remove();
 
  var html = ['<div class="tickLabels" style="font-size:smaller">'];
 
  var axes = allAxes();
  for (var j = 0; j < axes.length; ++j) {
  var axis = axes[j], box = axis.box;
  if (!axis.show)
  continue;
  //debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>')
  html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
  for (var i = 0; i < axis.ticks.length; ++i) {
  var tick = axis.ticks[i];
  if (!tick.label || tick.v < axis.min || tick.v > axis.max)
  continue;
 
  var pos = {}, align;
 
  if (axis.direction == "x") {
  align = "center";
  pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
  if (axis.position == "bottom")
  pos.top = box.top + box.padding;
  else
  pos.bottom = canvasHeight - (box.top + box.height - box.padding);
  }
  else {
  pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
  if (axis.position == "left") {
  pos.right = canvasWidth - (box.left + box.width - box.padding)
  align = "right";
  }
  else {
  pos.left = box.left + box.padding;
  align = "left";
  }
  }
 
  pos.width = axis.labelWidth;
 
  var style = ["position:absolute", "text-align:" + align ];
  for (var a in pos)
  style.push(a + ":" + pos[a] + "px")
 
  html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');
  }
  html.push('</div>');
  }
 
  html.push('</div>');
 
  placeholder.append(html.join(""));
  }
 
  function drawSeries(series) {
  if (series.lines.show)
  drawSeriesLines(series);
  if (series.bars.show)
  drawSeriesBars(series);
  if (series.points.show)
  drawSeriesPoints(series);
  }
 
  function drawSeriesLines(series) {
  function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
  var points = datapoints.points,
  ps = datapoints.pointsize,
  prevx = null, prevy = null;
 
  ctx.beginPath();
  for (var i = ps; i < points.length; i += ps) {
  var x1 = points[i - ps], y1 = points[i - ps + 1],
  x2 = points[i], y2 = points[i + 1];
 
  if (x1 == null || x2 == null)
  continue;
 
  // clip with ymin
  if (y1 <= y2 && y1 < axisy.min) {
  if (y2 < axisy.min)
  continue; // line segment is outside
  // compute new intersection point
  x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  y1 = axisy.min;
  }
  else if (y2 <= y1 && y2 < axisy.min) {
  if (y1 < axisy.min)
  continue;
  x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  y2 = axisy.min;
  }
 
  // clip with ymax
  if (y1 >= y2 && y1 > axisy.max) {
  if (y2 > axisy.max)
  continue;
  x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  y1 = axisy.max;
  }
  else if (y2 >= y1 && y2 > axisy.max) {
  if (y1 > axisy.max)
  continue;
  x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  y2 = axisy.max;
  }
 
  // clip with xmin
  if (x1 <= x2 && x1 < axisx.min) {
  if (x2 < axisx.min)
  continue;
  y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  x1 = axisx.min;
  }
  else if (x2 <= x1 && x2 < axisx.min) {
  if (x1 < axisx.min)
  continue;
  y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  x2 = axisx.min;
  }
 
  // clip with xmax
  if (x1 >= x2 && x1 > axisx.max) {
  if (x2 > axisx.max)
  continue;
  y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  x1 = axisx.max;
  }
  else if (x2 >= x1 && x2 > axisx.max) {
  if (x1 > axisx.max)
  continue;
  y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  x2 = axisx.max;
  }
 
  if (x1 != prevx || y1 != prevy)
  ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
 
  prevx = x2;
  prevy = y2;
  ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
  }
  ctx.stroke();
  }
 
  function plotLineArea(datapoints, axisx, axisy) {
  var points = datapoints.points,
  ps = datapoints.pointsize,
  bottom = Math.min(Math.max(0, axisy.min), axisy.max),
  i = 0, top, areaOpen = false,
  ypos = 1, segmentStart = 0, segmentEnd = 0;
 
  // we process each segment in two turns, first forward
  // direction to sketch out top, then once we hit the
  // end we go backwards to sketch the bottom
  while (true) {
  if (ps > 0 && i > points.length + ps)
  break;
 
  i += ps; // ps is negative if going backwards
 
  var x1 = points[i - ps],
  y1 = points[i - ps + ypos],
  x2 = points[i], y2 = points[i + ypos];
 
  if (areaOpen) {
  if (ps > 0 && x1 != null && x2 == null) {
  // at turning point
  segmentEnd = i;
  ps = -ps;
  ypos = 2;
  continue;
  }
 
  if (ps < 0 && i == segmentStart + ps) {
  // done with the reverse sweep
  ctx.fill();
  areaOpen = false;
  ps = -ps;
  ypos = 1;
  i = segmentStart = segmentEnd + ps;
  continue;
  }
  }
 
  if (x1 == null || x2 == null)
  continue;
 
  // clip x values
 
  // clip with xmin
  if (x1 <= x2 && x1 < axisx.min) {
  if (x2 < axisx.min)
  continue;
  y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  x1 = axisx.min;
  }
  else if (x2 <= x1 && x2 < axisx.min) {
  if (x1 < axisx.min)
  continue;
  y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  x2 = axisx.min;
  }
 
  // clip with xmax
  if (x1 >= x2 && x1 > axisx.max) {
  if (x2 > axisx.max)
  continue;
  y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  x1 = axisx.max;
  }
  else if (x2 >= x1 && x2 > axisx.max) {
  if (x1 > axisx.max)
  continue;
  y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  x2 = axisx.max;
  }
 
  if (!areaOpen) {
  // open area
  ctx.beginPath();
  ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
  areaOpen = true;
  }
 
  // now first check the case where both is outside
  if (y1 >= axisy.max && y2 >= axisy.max) {
  ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
  ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
  continue;
  }
  else if (y1 <= axisy.min && y2 <= axisy.min) {
  ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
  ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
  continue;
  }
 
  // else it's a bit more complicated, there might
  // be a flat maxed out rectangle first, then a
  // triangular cutout or reverse; to find these
  // keep track of the current x values
  var x1old = x1, x2old = x2;
 
  // clip the y values, without shortcutting, we
  // go through all cases in turn
 
  // clip with ymin
  if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
  x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  y1 = axisy.min;
  }
  else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
  x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  y2 = axisy.min;
  }
 
  // clip with ymax
  if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
  x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  y1 = axisy.max;
  }
  else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
  x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  y2 = axisy.max;
  }
 
  // if the x value was changed we got a rectangle
  // to fill
  if (x1 != x1old) {
  ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
  // it goes to (x1, y1), but we fill that below
  }
 
  // fill triangular section, this sometimes result
  // in redundant points if (x1, y1) hasn't changed
  // from previous line to, but we just ignore that
  ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
  ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
 
  // fill the other rectangle if it's there
  if (x2 != x2old) {
  ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
  ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
  }
  }
  }
 
  ctx.save();
  ctx.translate(plotOffset.left, plotOffset.top);
  ctx.lineJoin = "round";
 
  var lw = series.lines.lineWidth,
  sw = series.shadowSize;
  // FIXME: consider another form of shadow when filling is turned on
  if (lw > 0 && sw > 0) {
  // draw shadow as a thick and thin line with transparency
  ctx.lineWidth = sw;
  ctx.strokeStyle = "rgba(0,0,0,0.1)";
  // position shadow at angle from the mid of line
  var angle = Math.PI/18;
  plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
  ctx.lineWidth = sw/2;
  plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
  }
 
  ctx.lineWidth = lw;
  ctx.strokeStyle = series.color;
  var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
  if (fillStyle) {
  ctx.fillStyle = fillStyle;
  plotLineArea(series.datapoints, series.xaxis, series.yaxis);
  }
 
  if (lw > 0)
  plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
  ctx.restore();
  }
 
  function drawSeriesPoints(series) {
  function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
  var points = datapoints.points, ps = datapoints.pointsize;
 
  for (var i = 0; i < points.length; i += ps) {
  var x = points[i], y = points[i + 1];
  if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
  continue;
 
  ctx.beginPath();
  x = axisx.p2c(x);
  y = axisy.p2c(y) + offset;
  if (symbol == "circle")
  ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
  else
  symbol(ctx, x, y, radius, shadow);
  ctx.closePath();
 
  if (fillStyle) {
  ctx.fillStyle = fillStyle;
  ctx.fill();
  }
  ctx.stroke();
  }
  }
 
  ctx.save();
  ctx.translate(plotOffset.left, plotOffset.top);
 
  var lw = series.points.lineWidth,
  sw = series.shadowSize,
  radius = series.points.radius,
  symbol = series.points.symbol;
  if (lw > 0 && sw > 0) {
  // draw shadow in two steps
  var w = sw / 2;
  ctx.lineWidth = w;
  ctx.strokeStyle = "rgba(0,0,0,0.1)";
  plotPoints(series.datapoints, radius, null, w + w/2, true,
  series.xaxis, series.yaxis, symbol);
 
  ctx.strokeStyle = "rgba(0,0,0,0.2)";
  plotPoints(series.datapoints, radius, null, w/2, true,
  series.xaxis, series.yaxis, symbol);
  }
 
  ctx.lineWidth = lw;
  ctx.strokeStyle = series.color;
  plotPoints(series.datapoints, radius,
  getFillStyle(series.points, series.color), 0, false,
  series.xaxis, series.yaxis, symbol);
  ctx.restore();
  }
 
  function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
  var left, right, bottom, top,
  drawLeft, drawRight, drawTop, drawBottom,
  tmp;
 
  // in horizontal mode, we start the bar from the left
  // instead of from the bottom so it appears to be
  // horizontal rather than vertical
  if (horizontal) {
  drawBottom = drawRight = drawTop = true;
  drawLeft = false;
  left = b;
  right = x;
  top = y + barLeft;
  bottom = y + barRight;
 
  // account for negative bars
  if (right < left) {
  tmp = right;
  right = left;
  left = tmp;
  drawLeft = true;
  drawRight = false;
  }
  }
  else {
  drawLeft = drawRight = drawTop = true;
  drawBottom = false;
  left = x + barLeft;
  right = x + barRight;
  bottom = b;
  top = y;
 
  // account for negative bars
  if (top < bottom) {
  tmp = top;
  top = bottom;
  bottom = tmp;
  drawBottom = true;
  drawTop = false;
  }
  }
 
  // clip
  if (right < axisx.min || left > axisx.max ||
  top < axisy.min || bottom > axisy.max)
  return;
 
  if (left < axisx.min) {
  left = axisx.min;
  drawLeft = false;
  }
 
  if (right > axisx.max) {
  right = axisx.max;
  drawRight = false;
  }
 
  if (bottom < axisy.min) {
  bottom = axisy.min;
  drawBottom = false;
  }
 
  if (top > axisy.max) {
  top = axisy.max;
  drawTop = false;
  }
 
  left = axisx.p2c(left);
  bottom = axisy.p2c(bottom);
  right = axisx.p2c(right);
  top = axisy.p2c(top);
 
  // fill the bar
  if (fillStyleCallback) {
  c.beginPath();
  c.moveTo(left, bottom);
  c.lineTo(left, top);
  c.lineTo(right, top);
  c.lineTo(right, bottom);
  c.fillStyle = fillStyleCallback(bottom, top);
  c.fill();
  }
 
  // draw outline
  if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
  c.beginPath();
 
  // FIXME: inline moveTo is buggy with excanvas
  c.moveTo(left, bottom + offset);
  if (drawLeft)
  c.lineTo(left, top + offset);
  else
  c.moveTo(left, top + offset);
  if (drawTop)
  c.lineTo(right, top + offset);
  else
  c.moveTo(right, top + offset);
  if (drawRight)
  c.lineTo(right, bottom + offset);
  else
  c.moveTo(right, bottom + offset);
  if (drawBottom)
  c.lineTo(left, bottom + offset);
  else
  c.moveTo(left, bottom + offset);
  c.stroke();
  }
  }
 
  function drawSeriesBars(series) {
  function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
  var points = datapoints.points, ps = datapoints.pointsize;
 
  for (var i = 0; i < points.length; i += ps) {
  if (points[i] == null)
  continue;
  drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
  }
  }
 
  ctx.save();
  ctx.translate(plotOffset.left, plotOffset.top);
 
  // FIXME: figure out a way to add shadows (for instance along the right edge)
  ctx.lineWidth = series.bars.lineWidth;
  ctx.strokeStyle = series.color;
  var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
  var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
  plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
  ctx.restore();
  }
 
  function getFillStyle(filloptions, seriesColor, bottom, top) {
  var fill = filloptions.fill;
  if (!fill)
  return null;
 
  if (filloptions.fillColor)
  return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
 
  var c = $.color.parse(seriesColor);
  c.a = typeof fill == "number" ? fill : 0.4;
  c.normalize();
  return c.toString();
  }
 
  function insertLegend() {
  placeholder.find(".legend").remove();
 
  if (!options.legend.show)
  return;
 
  var fragments = [], rowStarted = false,
  lf = options.legend.labelFormatter, s, label;
  for (var i = 0; i < series.length; ++i) {
  s = series[i];
  label = s.label;
  if (!label)
  continue;
 
  if (i % options.legend.noColumns == 0) {
  if (rowStarted)
  fragments.push('</tr>');
  fragments.push('<tr>');
  rowStarted = true;
  }
 
  if (lf)
  label = lf(label, s);
 
  fragments.push(
  '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
  '<td class="legendLabel">' + label + '</td>');
  }
  if (rowStarted)
  fragments.push('</tr>');
 
  if (fragments.length == 0)
  return;
 
  var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
  if (options.legend.container != null)
  $(options.legend.container).html(table);
  else {
  var pos = "",
  p = options.legend.position,
  m = options.legend.margin;
  if (m[0] == null)
  m = [m, m];
  if (p.charAt(0) == "n")
  pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
  else if (p.charAt(0) == "s")
  pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
  if (p.charAt(1) == "e")
  pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
  else if (p.charAt(1) == "w")
  pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
  var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
  if (options.legend.backgroundOpacity != 0.0) {
  // put in the transparent background
  // separately to avoid blended labels and
  // label boxes
  var c = options.legend.backgroundColor;
  if (c == null) {
  c = options.grid.backgroundColor;
  if (c && typeof c == "string")
  c = $.color.parse(c);
  else
  c = $.color.extract(legend, 'background-color');
  c.a = 1;
  c = c.toString();
  }
  var div = legend.children();
  $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
  }
  }
  }
 
 
  // interactive features
 
  var highlights = [],
  redrawTimeout = null;
 
  // returns the data item the mouse is over, or null if none is found
  function findNearbyItem(mouseX, mouseY, seriesFilter) {
  var maxDistance = options.grid.mouseActiveRadius,
  smallestDistance = maxDistance * maxDistance + 1,
  item = null, foundPoint = false, i, j;
 
  for (i = series.length - 1; i >= 0; --i) {
  if (!seriesFilter(series[i]))
  continue;
 
  var s = series[i],
  axisx = s.xaxis,
  axisy = s.yaxis,
  points = s.datapoints.points,
  ps = s.datapoints.pointsize,
  mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
  my = axisy.c2p(mouseY),
  maxx = maxDistance / axisx.scale,
  maxy = maxDistance / axisy.scale;
 
  // with inverse transforms, we can't use the maxx/maxy
  // optimization, sadly
  if (axisx.options.inverseTransform)
  maxx = Number.MAX_VALUE;
  if (axisy.options.inverseTransform)
  maxy = Number.MAX_VALUE;
 
  if (s.lines.show || s.points.show) {
  for (j = 0; j < points.length; j += ps) {
  var x = points[j], y = points[j + 1];
  if (x == null)
  continue;
 
  // For points and lines, the cursor must be within a
  // certain distance to the data point
  if (x - mx > maxx || x - mx < -maxx ||
  y - my > maxy || y - my < -maxy)
  continue;
 
  // We have to calculate distances in pixels, not in
  // data units, because the scales of the axes may be different
  var dx = Math.abs(axisx.p2c(x) - mouseX),
  dy = Math.abs(axisy.p2c(y) - mouseY),
  dist = dx * dx + dy * dy; // we save the sqrt
 
  // use <= to ensure last point takes precedence
  // (last generally means on top of)
  if (dist < smallestDistance) {
  smallestDistance = dist;
  item = [i, j / ps];
  }
  }
  }
 
  if (s.bars.show && !item) { // no other point can be nearby
  var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
  barRight = barLeft + s.bars.barWidth;
 
  for (j = 0; j < points.length; j += ps) {
  var x = points[j], y = points[j + 1], b = points[j + 2];
  if (x == null)
  continue;
 
  // for a bar graph, the cursor must be inside the bar
  if (series[i].bars.horizontal ?
  (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
  my >= y + barLeft && my <= y + barRight) :
  (mx >= x + barLeft && mx <= x + barRight &&
  my >= Math.min(b, y) && my <= Math.max(b, y)))
  item = [i, j / ps];
  }
  }
  }
 
  if (item) {
  i = item[0];
  j = item[1];
  ps = series[i].datapoints.pointsize;
 
  return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
  dataIndex: j,
  series: series[i],
  seriesIndex: i };
  }
 
  return null;
  }
 
  function onMouseMove(e) {
  if (options.grid.hoverable)
  triggerClickHoverEvent("plothover", e,
  function (s) { return s["hoverable"] != false; });
  }
 
  function onMouseLeave(e) {
  if (options.grid.hoverable)
  triggerClickHoverEvent("plothover", e,
  function (s) { return false; });
  }
 
  function onClick(e) {
  triggerClickHoverEvent("plotclick", e,
  function (s) { return s["clickable"] != false; });
  }
 
  // trigger click or hover event (they send the same parameters
  // so we share their code)
  function triggerClickHoverEvent(eventname, event, seriesFilter) {
  var offset = eventHolder.offset(),
  canvasX = event.pageX - offset.left - plotOffset.left,
  canvasY = event.pageY - offset.top - plotOffset.top,
  pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
 
  pos.pageX = event.pageX;
  pos.pageY = event.pageY;
 
  var item = findNearbyItem(canvasX, canvasY, seriesFilter);
 
  if (item) {
  // fill in mouse pos for any listeners out there
  item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
  item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
  }
 
  if (options.grid.autoHighlight) {
  // clear auto-highlights
  for (var i = 0; i < highlights.length; ++i) {
  var h = highlights[i];
  if (h.auto == eventname &&
  !(item && h.series == item.series &&
  h.point[0] == item.datapoint[0] &&
  h.point[1] == item.datapoint[1]))
  unhighlight(h.series, h.point);
  }
 
  if (item)
  highlight(item.series, item.datapoint, eventname);
  }
 
  placeholder.trigger(eventname, [ pos, item ]);
  }
 
  function triggerRedrawOverlay() {
  if (!redrawTimeout)
  redrawTimeout = setTimeout(drawOverlay, 30);
  }
 
  function drawOverlay() {
  redrawTimeout = null;
 
  // draw highlights
  octx.save();
  octx.clearRect(0, 0, canvasWidth, canvasHeight);
  octx.translate(plotOffset.left, plotOffset.top);
 
  var i, hi;
  for (i = 0; i < highlights.length; ++i) {
  hi = highlights[i];
 
  if (hi.series.bars.show)
  drawBarHighlight(hi.series, hi.point);
  else
  drawPointHighlight(hi.series, hi.point);
  }
  octx.restore();
 
  executeHooks(hooks.drawOverlay, [octx]);
  }
 
  function highlight(s, point, auto) {
  if (typeof s == "number")
  s = series[s];
 
  if (typeof point == "number") {
  var ps = s.datapoints.pointsize;
  point = s.datapoints.points.slice(ps * point, ps * (point + 1));
  }
 
  var i = indexOfHighlight(s, point);
  if (i == -1) {
  highlights.push({ series: s, point: point, auto: auto });
 
  triggerRedrawOverlay();
  }
  else if (!auto)
  highlights[i].auto = false;
  }
 
  function unhighlight(s, point) {
  if (s == null && point == null) {
  highlights = [];
  triggerRedrawOverlay();
  }
 
  if (typeof s == "number")
  s = series[s];
 
  if (typeof point == "number")
  point = s.data[point];
 
  var i = indexOfHighlight(s, point);
  if (i != -1) {
  highlights.splice(i, 1);
 
  triggerRedrawOverlay();
  }
  }
 
  function indexOfHighlight(s, p) {
  for (var i = 0; i < highlights.length; ++i) {
  var h = highlights[i];
  if (h.series == s && h.point[0] == p[0]
  && h.point[1] == p[1])
  return i;
  }
  return -1;
  }
 
  function drawPointHighlight(series, point) {
  var x = point[0], y = point[1],
  axisx = series.xaxis, axisy = series.yaxis;
 
  if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
  return;
 
  var pointRadius = series.points.radius + series.points.lineWidth / 2;
  octx.lineWidth = pointRadius;
  octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
  var radius = 1.5 * pointRadius,
  x = axisx.p2c(x),
  y = axisy.p2c(y);
 
  octx.beginPath();
  if (series.points.symbol == "circle")
  octx.arc(x, y, radius, 0, 2 * Math.PI, false);
  else
  series.points.symbol(octx, x, y, radius, false);
  octx.closePath();
  octx.stroke();
  }
 
  function drawBarHighlight(series, point) {
  octx.lineWidth = series.bars.lineWidth;
  octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
  var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
  var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
  drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
  0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
  }
 
  function getColorOrGradient(spec, bottom, top, defaultColor) {
  if (typeof spec == "string")
  return spec;
  else {
  // assume this is a gradient spec; IE currently only
  // supports a simple vertical gradient properly, so that's
  // what we support too
  var gradient = ctx.createLinearGradient(0, top, 0, bottom);
 
  for (var i = 0, l = spec.colors.length; i < l; ++i) {
  var c = spec.colors[i];
  if (typeof c != "string") {
  var co = $.color.parse(defaultColor);
  if (c.brightness != null)
  co = co.scale('rgb', c.brightness)
  if (c.opacity != null)
  co.a *= c.opacity;
  c = co.toString();
  }
  gradient.addColorStop(i / (l - 1), c);
  }
 
  return gradient;
  }
  }
  }
 
  $.plot = function(placeholder, data, options) {
  //var t0 = new Date();
  var plot = new Plot($(placeholder), data, options, $.plot.plugins);
  //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
  return plot;
  };
 
  $.plot.version = "0.7";
 
  $.plot.plugins = [];
 
  // returns a string with the date d formatted according to fmt
  $.plot.formatDate = function(d, fmt, monthNames) {
  var leftPad = function(n) {
  n = "" + n;
  return n.length == 1 ? "0" + n : n;
  };
 
  var r = [];
  var escape = false, padNext = false;
  var hours = d.getUTCHours();
  var isAM = hours < 12;
  if (monthNames == null)
  monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
 
  if (fmt.search(/%p|%P/) != -1) {
  if (hours > 12) {
  hours = hours - 12;
  } else if (hours == 0) {
  hours = 12;
  }
  }
  for (var i = 0; i < fmt.length; ++i) {
  var c = fmt.charAt(i);
 
  if (escape) {
  switch (c) {
  case 'h': c = "" + hours; break;
  case 'H': c = leftPad(hours); break;
  case 'M': c = leftPad(d.getUTCMinutes()); break;
  case 'S': c = leftPad(d.getUTCSeconds()); break;
  case 'd': c = "" + d.getUTCDate(); break;
  case 'm': c = "" + (d.getUTCMonth() + 1); break;
  case 'y': c = "" + d.getUTCFullYear(); break;
  case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
  case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
  case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
  case '0': c = ""; padNext = true; break;
  }
  if (c && padNext) {
  c = leftPad(c);
  padNext = false;
  }
  r.push(c);
  if (!padNext)
  escape = false;
  }
  else {
  if (c == "%")
  escape = true;
  else
  r.push(c);
  }
  }
  return r.join("");
  };
 
  // round to nearby lower multiple of base
  function floorInBase(n, base) {
  return base * Math.floor(n / base);
  }
 
  })(jQuery);
 
  import datetime
 
  from pylons import config
  from sqlalchemy import Table, select, func, and_
  from sqlalchemy.sql.expression import text
 
  import ckan.plugins as p
  import ckan.model as model
 
  import re
 
  cache_enabled = p.toolkit.asbool(config.get('ckanext.stats.cache_enabled', 'True'))
 
  if cache_enabled:
  from pylons import cache
  our_cache = cache.get_cache('stats', type='dbm')
 
  DATE_FORMAT = '%Y-%m-%d'
 
  def table(name):
  return Table(name, model.meta.metadata, autoload=True)
 
  def datetime2date(datetime_):
  return datetime.date(datetime_.year, datetime_.month, datetime_.day)
 
 
  class Stats(object):
  @classmethod
  def top_rated_packages(cls, limit=10):
  # NB Not using sqlalchemy as sqla 0.4 doesn't work using both group_by
  # and apply_avg
  package = table('package')
  rating = table('rating')
  sql = select([package.c.id, func.avg(rating.c.rating), func.count(rating.c.rating)], from_obj=[package.join(rating)]).\
  where(package.c.private == 'f').\
  group_by(package.c.id).\
  order_by(func.avg(rating.c.rating).desc(), func.count(rating.c.rating).desc()).\
  limit(limit)
  res_ids = model.Session.execute(sql).fetchall()
  res_pkgs = [(model.Session.query(model.Package).get(unicode(pkg_id)), avg, num) for pkg_id, avg, num in res_ids]
  return res_pkgs
 
  @classmethod
  def most_edited_packages(cls, limit=10):
  package_revision = table('package_revision')
  package = table('package')
  s = select([package_revision.c.id, func.count(package_revision.c.revision_id)], from_obj=[package_revision.join(package)]).\
  where(package.c.private == 'f').\
  group_by(package_revision.c.id).\
  order_by(func.count(package_revision.c.revision_id).desc()).\
  limit(limit)
  res_ids = model.Session.execute(s).fetchall()
  res_pkgs = [(model.Session.query(model.Package).get(unicode(pkg_id)), val) for pkg_id, val in res_ids]
  return res_pkgs
 
  @classmethod
  def largest_groups(cls, limit=10):
  member = table('member')
  s = select([member.c.group_id, func.count(member.c.table_id)]).\
  group_by(member.c.group_id).\
  where(member.c.group_id!=None).\
  where(member.c.table_name=='package').\
  where(member.c.capacity=='public').\
  order_by(func.count(member.c.table_id).desc())
  #limit(limit)
 
  res_ids = model.Session.execute(s).fetchall()
  res_groups = [(model.Session.query(model.Group).get(unicode(group_id)), val) for group_id, val in res_ids]
  return res_groups
 
  @classmethod
  def by_org(cls, limit=10):
  connection = model.Session.connection()
  res = connection.execute("select package.owner_org, package.private, count(*) from package \
  inner join \"group\" on package.owner_org = \"group\".id \
  where package.state='active'\
  group by package.owner_org,\"group\".name, package.private \
  order by \"group\".name, package.private;").fetchall();
  res_groups = [(model.Session.query(model.Group).get(unicode(group_id)), private, val) for group_id, private, val in res]
  return res_groups
 
  @classmethod
  def res_by_org(cls, limit=10):
  connection = model.Session.connection()
  reses = connection.execute("select owner_org,format,count(*) from \
  resource inner join resource_group on resource.resource_group_id = resource_group.id \
  inner join package on resource_group.package_id = package.id group by owner_org,format order by count desc;").fetchall();
  group_ids = []
  group_tab = {}
  group_spatial = {}
  group_other = {}
  for group_id,format,count in reses:
  if group_id not in group_ids:
  group_ids.append(group_id)
  group_tab[group_id] = 0
  group_spatial[group_id] = 0
  group_other[group_id] = 0
  if re.search('xls|csv|ms-excel|spreadsheetml.sheet|zip|netcdf',format, re.IGNORECASE):
  group_tab[group_id] = group_tab[group_id] + count
  elif re.search('wms|wfs|wcs|shp|kml|kmz',format, re.IGNORECASE):
  group_spatial[group_id] = group_spatial[group_id] + count
  else:
  group_other[group_id] = group_other[group_id] + count
  return [(model.Session.query(model.Group).get(unicode(group_id)), group_tab[group_id],group_spatial[group_id],group_other[group_id], group_tab[group_id]+group_spatial[group_id]+group_other[group_id]) for group_id in group_ids]
 
  @classmethod
  def top_active_orgs(cls, limit=10):
  connection = model.Session.connection()
  res = connection.execute("select package.owner_org, count(*) from package \
  inner join \"group\" on package.owner_org = \"group\".id \
  inner join (select distinct object_id from activity where activity.timestamp > (now() - interval '60 day')) \
  latestactivities on latestactivities.object_id = package.id \
  where package.state='active' \
  and package.private = 'f' \
  group by package.owner_org \
  order by count(*) desc;").fetchall();
  res_groups = [(model.Session.query(model.Group).get(unicode(group_id)), val) for group_id, val in res]
  return res_groups
 
  @classmethod
  def top_tags(cls, limit=10, returned_tag_info='object'): # by package
  assert returned_tag_info in ('name', 'id', 'object')
  tag = table('tag')
  package_tag = table('package_tag')
  package = table('package')
  #TODO filter out tags with state=deleted
  if returned_tag_info == 'name':
  from_obj = [package_tag.join(tag)]
  tag_column = tag.c.name
  else:
  from_obj = None
  tag_column = package_tag.c.tag_id
  s = select([tag_column, func.count(package_tag.c.package_id)],
  from_obj=from_obj)
  s = s.group_by(tag_column).\
  where(package.c.private == 'f').\
  order_by(func.count(package_tag.c.package_id).desc()).\
  limit(limit)
  res_col = model.Session.execute(s).fetchall()
  if returned_tag_info in ('id', 'name'):
  return res_col
  elif returned_tag_info == 'object':
  res_tags = [(model.Session.query(model.Tag).get(unicode(tag_id)), val) for tag_id, val in res_col]
  return res_tags
 
  @classmethod
  def top_package_owners(cls, limit=10):
  package_role = table('package_role')
  user_object_role = table('user_object_role')
  package = table('package')
  s = select([user_object_role.c.user_id, func.count(user_object_role.c.role)], from_obj=[user_object_role.join(package_role).join(package, package_role.c.package_id == package.c.id)]).\
  where(user_object_role.c.role==model.authz.Role.ADMIN).\
  where(package.c.private == 'f').\
  where(user_object_role.c.user_id!=None).\
  group_by(user_object_role.c.user_id).\
  order_by(func.count(user_object_role.c.role).desc()).\
  limit(limit)
  res_ids = model.Session.execute(s).fetchall()
  res_users = [(model.Session.query(model.User).get(unicode(user_id)), val) for user_id, val in res_ids]
  return res_users
 
  @classmethod
  def summary_stats(cls):
  connection = model.Session.connection()
 
  res = connection.execute("SELECT 'Total Organisations', count(*) from \"group\" where type = 'organization' and state = 'active' union \
  select 'Total Datasets', count(*) from package where (state='active' or state='draft' or state='draft-complete') and private = 'f' union \
  select 'Total Archived Datasets', count(*) from package where (state='active' or state='draft' or state='draft-complete') and private = 't' union \
  select 'Total Data Files/Resources', count(*) from resource where state='active' union \
  select 'Total Machine Readable/Data API Resources', count(*) from resource where state='active' and webstore_url = 'active'\
  ").fetchall();
  return res
 
 
  @classmethod
  def activity_counts(cls):
  connection = model.Session.connection()
  res = connection.execute("select to_char(timestamp, 'YYYY-MM') as month,activity_type, count(*) from activity group by month, activity_type order by month;").fetchall();
  return res
 
  @classmethod
  def user_access_list(cls):
  connection = model.Session.connection()
  res = connection.execute("select name,sysadmin,role from user_object_role right outer join \"user\" on user_object_role.user_id = \"user\".id where name not in ('logged_in','visitor') group by name,sysadmin,role order by sysadmin desc, role asc;").fetchall();
  return res
 
  @classmethod
  def recent_datasets(cls):
  activity = table('activity')
  package = table('package')
  s = select([func.max(activity.c.timestamp),package.c.id, activity.c.activity_type], from_obj=[activity.join(package,activity.c.object_id == package.c.id)]).where(package.c.private == 'f').\
  where(activity.c.timestamp > func.now() - text("interval '60 day'")).group_by(package.c.id,activity.c.activity_type).order_by(func.max(activity.c.timestamp))
  result = model.Session.execute(s).fetchall()
  return [(datetime2date(timestamp), model.Session.query(model.Package).get(unicode(package_id)), activity_type) for timestamp,package_id,activity_type in result]
 
 
 
  class RevisionStats(object):
  @classmethod
  def package_addition_rate(cls, weeks_ago=0):
  week_commenced = cls.get_date_weeks_ago(weeks_ago)
  return cls.get_objects_in_a_week(week_commenced,
  type_='package_addition_rate')
 
  @classmethod
  def package_revision_rate(cls, weeks_ago=0):
  week_commenced = cls.get_date_weeks_ago(weeks_ago)
  return cls.get_objects_in_a_week(week_commenced,
  type_='package_revision_rate')
 
  @classmethod
  def get_date_weeks_ago(cls, weeks_ago):
  '''
  @param weeks_ago: specify how many weeks ago to give count for
  (0 = this week so far)
  '''
  date_ = datetime.date.today()
  return date_ - datetime.timedelta(days=
  datetime.date.weekday(date_) + 7 * weeks_ago)
 
  @classmethod
  def get_week_dates(cls, weeks_ago):
  '''
  @param weeks_ago: specify how many weeks ago to give count for
  (0 = this week so far)
  '''
  package_revision = table('package_revision')
  revision = table('revision')
  today = datetime.date.today()
  date_from = datetime.datetime(today.year, today.month, today.day) -\
  datetime.timedelta(days=datetime.date.weekday(today) + \
  7 * weeks_ago)
  date_to = date_from + datetime.timedelta(days=7)
  return (date_from, date_to)
 
  @classmethod
  def get_date_week_started(cls, date_):
  assert isinstance(date_, datetime.date)
  if isinstance(date_, datetime.datetime):
  date_ = datetime2date(date_)
  return date_ - datetime.timedelta(days=datetime.date.weekday(date_))
 
  @classmethod
  def get_package_revisions(cls):
  '''
  @return: Returns list of revisions and date of them, in
  format: [(id, date), ...]
  '''
  package_revision = table('package_revision')
  revision = table('revision')
  s = select([package_revision.c.id, revision.c.timestamp], from_obj=[package_revision.join(revision)]).order_by(revision.c.timestamp)
  res = model.Session.execute(s).fetchall() # [(id, datetime), ...]
  return res
 
  @classmethod
  def get_new_packages(cls):
  '''
  @return: Returns list of new pkgs and date when they were created, in
  format: [(id, date_ordinal), ...]
  '''
  def new_packages():
  # Can't filter by time in select because 'min' function has to
  # be 'for all time' else you get first revision in the time period.
  package_revision = table('package_revision')
  revision = table('revision')
  package = table('package')
  s = select([package_revision.c.id, func.min(revision.c.timestamp)], from_obj=[package_revision.join(revision).join(package)]).\
  where(package.c.private == 'f').\
  group_by(package_revision.c.id).order_by(func.min(revision.c.timestamp))
  res = model.Session.execute(s).fetchall() # [(id, datetime), ...]
  res_pickleable = []
  for pkg_id, created_datetime in res:
  res_pickleable.append((pkg_id, created_datetime.toordinal()))
  return res_pickleable
  if cache_enabled:
  week_commences = cls.get_date_week_started(datetime.date.today())
  key = 'all_new_packages_%s' + week_commences.strftime(DATE_FORMAT)
  new_packages = our_cache.get_value(key=key,
  createfunc=new_packages)
  else:
  new_packages = new_packages()
  return new_packages
 
  @classmethod
  def get_deleted_packages(cls):
  '''
  @return: Returns list of deleted pkgs and date when they were deleted, in
  format: [(id, date_ordinal), ...]
  '''
  def deleted_packages():
  # Can't filter by time in select because 'min' function has to
  # be 'for all time' else you get first revision in the time period.
  package_revision = table('package_revision')
  revision = table('revision')
  package = table('package')
  s = select([package_revision.c.id, func.min(revision.c.timestamp)], from_obj=[package_revision.join(revision).join(package)]).\
  where(package_revision.c.state==model.State.DELETED).\
  where(package.c.private == 'f').\
  group_by(package_revision.c.id).\
  order_by(func.min(revision.c.timestamp))
  res = model.Session.execute(s).fetchall() # [(id, datetime), ...]
  res_pickleable = []
  for pkg_id, deleted_datetime in res:
  res_pickleable.append((pkg_id, deleted_datetime.toordinal()))
  return res_pickleable
  if cache_enabled:
  week_commences = cls.get_date_week_started(datetime.date.today())
  key = 'all_deleted_packages_%s' + week_commences.strftime(DATE_FORMAT)
  deleted_packages = our_cache.get_value(key=key,
  createfunc=deleted_packages)
  else:
  deleted_packages = deleted_packages()
  return deleted_packages
 
  @classmethod
  def get_num_packages_by_week(cls):
  def num_packages():
  new_packages_by_week = cls.get_by_week('new_packages')
  deleted_packages_by_week = cls.get_by_week('deleted_packages')
  first_date = (min(datetime.datetime.strptime(new_packages_by_week[0][0], DATE_FORMAT),
  datetime.datetime.strptime(deleted_packages_by_week[0][0], DATE_FORMAT))).date()
  cls._cumulative_num_pkgs = 0
  new_pkgs = []
  deleted_pkgs = []
  def build_weekly_stats(week_commences, new_pkg_ids, deleted_pkg_ids):
  num_pkgs = len(new_pkg_ids) - len(deleted_pkg_ids)
  new_pkgs.extend([model.Session.query(model.Package).get(id).name for id in new_pkg_ids])
  deleted_pkgs.extend([model.Session.query(model.Package).get(id).name for id in deleted_pkg_ids])
  cls._cumulative_num_pkgs += num_pkgs
  return (week_commences.strftime(DATE_FORMAT),
  num_pkgs, cls._cumulative_num_pkgs)
  week_ends = first_date
  today = datetime.date.today()
  new_package_week_index = 0
  deleted_package_week_index = 0
  weekly_numbers = [] # [(week_commences, num_packages, cumulative_num_pkgs])]
  while week_ends <= today:
  week_commences = week_ends
  week_ends = week_commences + datetime.timedelta(days=7)
  if datetime.datetime.strptime(new_packages_by_week[new_package_week_index][0], DATE_FORMAT).date() == week_commences:
  new_pkg_ids = new_packages_by_week[new_package_week_index][1]
  new_package_week_index += 1
  else:
  new_pkg_ids = []
  if datetime.datetime.strptime(deleted_packages_by_week[deleted_package_week_index][0], DATE_FORMAT).date() == week_commences:
  deleted_pkg_ids = deleted_packages_by_week[deleted_package_week_index][1]
  deleted_package_week_index += 1
  else:
  deleted_pkg_ids = []
  weekly_numbers.append(build_weekly_stats(week_commences, new_pkg_ids, deleted_pkg_ids))
  # just check we got to the end of each count
  assert new_package_week_index == len(new_packages_by_week)
  assert deleted_package_week_index == len(deleted_packages_by_week)
  return weekly_numbers
  if cache_enabled:
  week_commences = cls.get_date_week_started(datetime.date.today())
  key = 'number_packages_%s' + week_commences.strftime(DATE_FORMAT)
  num_packages = our_cache.get_value(key=key,
  createfunc=num_packages)
  else:
  num_packages = num_packages()
  return num_packages
 
  @classmethod
  def get_by_week(cls, object_type):
  cls._object_type = object_type
  def objects_by_week():
  if cls._object_type == 'new_packages':
  objects = cls.get_new_packages()
  def get_date(object_date):
  return datetime.date.fromordinal(object_date)
  elif cls._object_type == 'deleted_packages':
  objects = cls.get_deleted_packages()
  def get_date(object_date):
  return datetime.date.fromordinal(object_date)
  elif cls._object_type == 'package_revisions':
  objects = cls.get_package_revisions()
  def get_date(object_date):
  return datetime2date(object_date)
  else:
  raise NotImplementedError()
  first_date = get_date(objects[0][1]) if objects else datetime.date.today()
  week_commences = cls.get_date_week_started(first_date)
  week_ends = week_commences + datetime.timedelta(days=7)
  week_index = 0
  weekly_pkg_ids = [] # [(week_commences, [pkg_id1, pkg_id2, ...])]
  pkg_id_stack = []
  cls._cumulative_num_pkgs = 0
  def build_weekly_stats(week_commences, pkg_ids):
  num_pkgs = len(pkg_ids)
  cls._cumulative_num_pkgs += num_pkgs
  return (week_commences.strftime(DATE_FORMAT),
  pkg_ids, num_pkgs, cls._cumulative_num_pkgs)
  for pkg_id, date_field in objects:
  date_ = get_date(date_field)
  if date_ >= week_ends:
  weekly_pkg_ids.append(build_weekly_stats(week_commences, pkg_id_stack))
  pkg_id_stack = []
  week_commences = week_ends
  week_ends = week_commences + datetime.timedelta(days=7)
  pkg_id_stack.append(pkg_id)
  weekly_pkg_ids.append(build_weekly_stats(week_commences, pkg_id_stack))
  today = datetime.date.today()
  while week_ends <= today:
  week_commences = week_ends
  week_ends = week_commences + datetime.timedelta(days=7)
  weekly_pkg_ids.append(build_weekly_stats(week_commences, []))
  return weekly_pkg_ids
  if cache_enabled:
  week_commences = cls.get_date_week_started(datetime.date.today())
  key = '%s_by_week_%s' % (cls._object_type, week_commences.strftime(DATE_FORMAT))
  objects_by_week_ = our_cache.get_value(key=key,
  createfunc=objects_by_week)
  else:
  objects_by_week_ = objects_by_week()
  return objects_by_week_
 
  @classmethod
  def get_objects_in_a_week(cls, date_week_commences,
  type_='new-package-rate'):
  '''
  @param type: Specifies what to return about the specified week:
  "package_addition_rate" number of new packages
  "package_revision_rate" number of package revisions
  "new_packages" a list of the packages created
  in a tuple with the date.
  "deleted_packages" a list of the packages deleted
  in a tuple with the date.
  @param dates: date range of interest - a tuple:
  (start_date, end_date)
  '''
  assert isinstance(date_week_commences, datetime.date)
  if type_ in ('package_addition_rate', 'new_packages'):
  object_type = 'new_packages'
  elif type_ == 'deleted_packages':
  object_type = 'deleted_packages'
  elif type_ == 'package_revision_rate':
  object_type = 'package_revisions'
  else:
  raise NotImplementedError()
  objects_by_week = cls.get_by_week(object_type)
  date_wc_str = date_week_commences.strftime(DATE_FORMAT)
  object_ids = None
  for objects_in_a_week in objects_by_week:
  if objects_in_a_week[0] == date_wc_str:
  object_ids = objects_in_a_week[1]
  break
  if object_ids is None:
  raise TypeError('Week specified is outside range')
  assert isinstance(object_ids, list)
  if type_ in ('package_revision_rate', 'package_addition_rate'):
  return len(object_ids)
  elif type_ in ('new_packages', 'deleted_packages'):
  return [ model.Session.query(model.Package).get(pkg_id) \
  for pkg_id in object_ids ]
 
 
  {% extends "page.html" %}
 
  {% block breadcrumb_content %}
  <li class="active">{{ 'Statistics' }}</li>
  {% endblock %}
 
  {% block primary_content %}
  <article class="module">
  {% if h.check_access('sysadmin') %}
  <section id="stats-activity-counts" class="module-content tab-content">
  <h2>{{ _('Site Activity Log') }}</h2>
  {% if c.activity_counts %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Month') }}</th>
  <th>{{ _('Activity Type') }}</th>
  <th class="metric">{{ _('Count') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for month, type, count in c.activity_counts %}
  <tr>
  <td>{{ month }}</td>
  <td>{{ type }}</td>
  <td class="metric">{{ count }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  <section id="stats-recent-datasets" class="module-content tab-content">
  <h2>{{ _('Recent Datasets') }}</h2>
  {% if c.recent_datasets %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Date') }}</th>
  <th>{{ _('Dataset') }}</th>
  <th>{{ _('New/Modified') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for date,package,newmodified in c.recent_datasets %}
  <tr>
  <td>{{ date }}</td>
  <td>{{ h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name)) }}</td>
  <td>{{ newmodified }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  <section id="stats-user-access-list" class="module-content tab-content">
  <h2>{{ _('User Access List') }}</h2>
  {% if c.user_access_list %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Username') }}</th>
  <th>{{ _('Sysadmin') }}</th>
  <th class="metric">{{ _('Organisational Role') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for username,sysadmin,role in c.user_access_list %}
  <tr>
  <td>{{ username }}</td>
  <td>{{ sysadmin }}</td>
  <td>{{ role }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  {% endif %}
  <section id="stats-total-datasets" class="module-content tab-content active">
  <h2>{{ _('Total number of Datasets') }}</h2>
 
  {% set xaxis = {'mode': 'time', 'timeformat': '%y-%b'} %}
  {% set yaxis = {'min': 0} %}
  <table class="table table-chunky table-bordered table-striped" data-module="plot" data-module-xaxis="{{ h.dump_json(xaxis) }}" data-module-yaxis="{{ h.dump_json(yaxis) }}">
  <thead>
  <tr>
  <th>{{ _("Date") }}</th>
  <th>{{ _("Total datasets") }}</th>
  </tr>
  </thead>
  <tbody>
  {% for row in c.raw_packages_by_week %}
  <tr>
  <th data-type="date" data-value="{{ row.date.strftime("%s") }}"><time datetime="{{ row.date.isoformat() }}">{{ h.render_datetime(row.date) }}</time></th>
  <td>{{ row.total_packages }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  </section>
 
  <section id="stats-dataset-revisions" class="module-content tab-content">
  <h2>{{ _('Dataset Revisions per Week') }}</h2>
 
  {% set xaxis = {'mode': 'time', 'timeformat': '%y-%b'} %}
  {% set lines = {'fill': 1} %}
  <table class="table table-chunky table-bordered table-striped" data-module="plot" data-module-xaxis="{{ h.dump_json(xaxis) }}" data-module-lines="{{ h.dump_json(lines) }}">
  <thead>
  <tr>
  <th>{{ _("Date") }}</th>
  <th>{{ _("All dataset revisions") }}</th>
  <th>{{ _("New datasets") }}</th>
  </tr>
  </thead>
  <tbody>
  {% for row in c.raw_all_package_revisions %}
  <tr>
  <th data-type="date" data-value="{{ row.date.strftime("%s") }}"><time datetime="{{ row.date.isoformat() }}">{{ h.render_datetime(row.date) }}</time></th>
  <td>{{ row.total_revisions }}</td>
  <td>{{ c.raw_new_datasets[loop.index0].new_packages }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  </section>
 
 
  <section id="stats-most-edited" class="module-content tab-content">
  <h2>{{ _('Most Edited Datasets') }}</h2>
  {% if c.most_edited_packages %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Dataset') }}</th>
  <th class="metric">{{ _('Number of edits') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for package, edits in c.most_edited_packages %}
  <tr py:for="package, edits in c.most_edited_packages">
  <td>{{ h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name)) }}</td>
  <td class="metric">{{ edits }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No edited datasets') }}</p>
  {% endif %}
  </section>
 
  <section id="stats-largest-groups" class="module-content tab-content">
  <h2>{{ _('Largest Groups') }}</h2>
  {% if c.largest_groups %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Group') }}</th>
  <th class="metric">{{ _('Number of datasets') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for group, num_packages in c.largest_groups %}
  <tr>
  <td>{{ h.link_to(group.title or group.name, h.url_for(controller='group', action='read', id=group.name)) }}</td>
  <td class="metric">{{ num_packages }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  <section id="stats-by-org" class="module-content tab-content">
  <h2>{{ _('Datasets by Organization') }}</h2>
  {% if c.by_org %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Organisation') }}</th>
  <th>{{ _('Public/Archived') }}</th>
  <th class="metric">{{ _('Number of datasets') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for group,private, num_packages in c.by_org %}
  {% if private == False or h.check_access('sysadmin') %}
  <tr>
  <td>{{ h.link_to(group.title or group.name, h.url_for(controller='organization', action='read', id=group.name)) }}</td>
  {% if private == True %}
  <td>Archived</td>
  {% else %}
  <td>Public</td>
  {% endif %}
  <td class="metric">{{ num_packages }}</td>
  </tr>
  {% endif %}
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  <section id="stats-res-by-org" class="module-content tab-content">
  <h2>{{ _('Resources by Organization') }}</h2>
  {% if c.res_by_org %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Organisation') }}</th>
  <th>{{ _('Tabular') }}</th>
  <th>{{ _('Spatial') }}</th>
  <th>{{ _('Other') }}</th>
  <th class="metric">{{ _('Total') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for group,t,s,o,tot in c.res_by_org %}
  <tr>
  <td>{{ h.link_to(group.title or group.name, h.url_for(controller='organization', action='read', id=group.name)) }}</td>
  <td>{{ t }}</td>
  <td>{{ s }}</td>
  <td>{{ o }}</td>
  <td class="metric">{{ tot }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  <section id="stats-activity-org" class="module-content tab-content">
  <h2>{{ _('Most Active Organisations') }}</h2>
  {% if c.top_active_orgs %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Organisation') }}</th>
  <th class="metric">{{ _('Number of datasets updated recently') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for group, num_packages in c.top_active_orgs %}
  <tr>
  <td>{{ h.link_to(group.title or group.name, h.url_for(controller='organization', action='read', id=group.name)) }}</td>
  <td class="metric">{{ num_packages }}</td>
  </tr>
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  <section id="stats-summary" class="module-content tab-content">
  <h2>{{ _('Summary') }}</h2>
  {% if c.summary_stats %}
  <table class="table table-chunky table-bordered table-striped">
  <thead>
  <tr>
  <th>{{ _('Measure') }}</th>
  <th class="metric">{{ _('Value') }}</th>
  </tr>
  </thead>
  <tbody>
  {% for measure,value in c.summary_stats %}
  {% if 'Archived' not in measure or h.check_access('sysadmin') %}
  <tr>
  <td>{{measure}}</td>
  <td class="metric">{{ value }}</td>
  </tr>
  {% endif %}
  {% endfor %}
  </tbody>
  </table>
  {% else %}
  <p class="empty">{{ _('No groups') }}</p>
  {% endif %}
  </section>
  </article>
  {% endblock %}
 
  {% block secondary_content %}
  <section class="module module-narrow">
  <h2 class="module-heading"><i class="icon-bar-chart icon-medium"></i> {{ _('Statistics Menu') }}</h2>
  <nav data-module="stats-nav">
  <ul class="unstyled nav nav-simple">
  {% if h.check_access('sysadmin') %}
  <li class="nav-item"><a href="#stats-recent-datasets" data-toggle="tab">{{ _('Recent Datasets') }}</a></li>
  <li class="nav-item"><a href="#stats-user-access-list" data-toggle="tab">{{ _('User Access List') }}</a></li>
  {% endif %}
  <li class="nav-item"><a href="#stats-total-datasets" data-toggle="tab">{{ _('Total Number of Datasets') }}</a></li>
  <li class="nav-item"><a href="#stats-dataset-revisions" data-toggle="tab">{{ _('Dataset Revisions per Week') }}</a></li>
  <li class="nav-item"><a href="#stats-most-edited" data-toggle="tab">{{ _('Most Edited Datasets') }}</a></li>
  <li class="nav-item"><a href="#stats-by-org" data-toggle="tab">{{ _('Datasets by Organization') }}</a></li>
  <li class="nav-item"><a href="#stats-res-by-org" data-toggle="tab">{{ _('Resources by Organization') }}</a></li>
  <li class="nav-item"><a href="#stats-activity-org" data-toggle="tab">{{ _('Most Active Organisations') }}</a></li>
  <li class="nav-item active"><a href="#stats-summary" data-toggle="tab">{{ _('Summary') }}</a></li>
  </ul>
  </nav>
  </section>
  {% endblock %}
 
  {% block scripts %}
  {{ super() }}
  {#
  Hellish hack to get excanvas to work in IE8. We disable html5shiv from
  overriding the createElement() method on this page.
  See: http://stackoverflow.com/questions/10208062/using-flot-with-bootstrap-ie8-incompatibility
  #}
  {% resource "vendor/block_html5_shim" %}
  {% resource "ckanext_dga_stats/stats" %}
  {% endblock %}
 
  <html xmlns:py="http://genshi.edgewall.org/"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip="">
 
  <py:def i18n:msg="" function="page_title">Statistics</py:def>
 
  <py:def function="page_heading">
  Statistics
  </py:def>
 
  <py:def function="optional_feed">
  <!--!
  Hellish hack to get excanvas to work in IE8. We disable html5shiv from
  overriding the createElement() method on this page.
  See: http://stackoverflow.com/questions/10208062/using-flot-with-bootstrap-ie8-incompatibility
  -->
  <!--[if lte IE 8 ]><script>var html5 = {shivMethods: false};</script><![endif]-->
  </py:def>
 
  <py:def function="optional_head">
  <style type="text/css">
  body #sidebar {
  display: none;
  }
 
  body #content {
  width: 950px;
  }
 
  h3 {
  margin-top: 20px;
  }
 
  .graph {
  width: 950px;
  height: 300px;
  margin-bottom: 20px;
  }
 
  .metric {
  width: 30%;
  }
 
  </style>
  </py:def>
 
  <py:match path="minornavigation">
  <ul class="tabbed">
  <li class="current-tab">
  <a href="">Home</a>
  </li>
  </ul>
  </py:match>
 
  <div py:match="content">
  <h3>Total number of Datasets</h3>
  <div id="new_packages_graph" class="graph"></div>
 
  <h3>Revisions to Datasets per week</h3>
  <div id="package_revisions_graph" class="graph"></div>
 
  <h3>Top Rated Datasets</h3>
  <table py:if="c.top_rated_packages" class="table table-bordered table-striped">
  <tr><th>Dataset</th><th>Average rating</th><th class="metric">Number of ratings</th></tr>
  <tr py:for="package, rating, num_ratings in c.top_rated_packages">
  <td>${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}</td><td>${rating}</td><td>${num_ratings}</td>
  </tr>
  </table>
  <p py:if="not c.top_rated_packages">No ratings</p>
 
  <h3>Most Edited Datasets</h3>
  <table class="table table-bordered table-striped">
  <tr><th>Dataset</th><th class="metric">Number of edits</th></tr>
  <tr py:for="package, edits in c.most_edited_packages">
  <td>${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}</td><td>${edits}</td>
  </tr>
  </table>
 
  <h3>Largest Groups</h3>
  <table class="table table-bordered table-striped">
  <tr><th>Group</th><th class="metric">Number of datasets</th></tr>
  <tr py:for="group, num_packages in c.largest_groups">
  <td>${h.link_to(group.title or group.name, h.url_for(controller='group', action='read', id=group.name))}</td><td>${num_packages}</td>
  </tr>
  </table>
 
  <h3>Top Tags</h3>
  <table class="table table-bordered table-striped">
  <tr py:for="tag, num_packages in c.top_tags">
  <td>${h.link_to(tag.name, h.url_for(controller='tag', action='read', id=tag.name))}</td><td class="metric">${num_packages}</td>
  </tr>
  </table>
 
  <h3>Users owning most datasets</h3>
  <table class="table table-bordered table-striped">
  <tr py:for="user, num_packages in c.top_package_owners">
  <td>${h.linked_user(user)}</td><td class="metric">${num_packages}</td>
  </tr>
  </table>
 
  <p>
  Page last updated:
  <?python
  import datetime
  ?>
  ${datetime.datetime.now().strftime('%c')}
  </p>
  </div>
 
  <py:def function="optional_footer">
  <script type="text/javascript">
  // HACKy
  $('body').addClass('no-sidebar');
  </script>
 
  ${jsConditionalForIe(8, '&lt;script language="javascript" type="text/javascript" src="' + h.url_for_static('/scripts/vendor/flot/0.7/excanvas.js') + '"&gt;&lt;/script&gt;', 'lte')}
  <script type="text/javascript" src="${ h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js') }">//pointless jscript comment</script>
  <script type="text/javascript">
  var options = {
  xaxis: {
  mode: "time",
  timeformat: "%y-%b"
  },
  yaxis: {
  min: 0
  },
  legend: {
  position: "nw"
  }
  };
  var data = [
  [
  ${",".join(c.packages_by_week)}
  ]
  ];
  $.plot($("#new_packages_graph"), data, options);
  </script>
 
  <script type="text/javascript">
  var options = {
  xaxis: {
  mode: "time",
  timeformat: "%y-%b"
  },
  legend: {
  position: "nw"
  },
  colors: ["#ffcc33", "#ff8844"]
  };
  var data = [
  {label: "All package revisions",
  lines: {fill: 1 },
  data: [${",".join(c.all_package_revisions)}]},
  {label: "New datasets",
  lines: {fill: 1},
  data: [${",".join(c.new_datasets)}]}
  ];
  $.plot($("#package_revisions_graph"), data, options);
  </script>
  </py:def>
  <xi:include href="../../layout.html" />
  </html>
 
  <html xmlns:py="http://genshi.edgewall.org/"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip="">
 
  <py:def function="page_title">Leaderboard - Stats</py:def>
 
  <py:def function="optional_head">
  <script type="text/javascript">
  var solrCoreUrl = '${c.solr_core_url}';
  </script>
  <script type="text/javascript" src="${h.url_for('/ckanext/stats/app.js')}"></script>
  <link type="text/css" rel="stylesheet" media="all" href="${h.url_for('/ckanext/stats/style.css')}" />
  </py:def>
 
  <div py:match="content">
  <h2>Dataset Leaderboard</h2>
  <p>Choose a dataset attribute and find out which categories in that area have the most datasets. E.g. tags, groups, license, res_format, country.</p>
  <form>
  <label for="category">Choose area</label>
  <input type="text" value="tags" name="attribute" id="form-attribute" />
  <input type="submit" value="Dataset Counts &raquo;" name="submit" />
  </form>
 
  <div class="category-counts">
  <ul class="chartlist" id="category-counts">
  </ul>
  </div>
  </div>
 
  <xi:include href="../../layout.html" />
  </html>
 
 
  import paste.fixture
  from pylons import config
  from ckan.config.middleware import make_app
 
  class StatsFixture(object):
 
  @classmethod
  def setup_class(cls):
  cls._original_config = config.copy()
  config['ckan.plugins'] = 'dga_stats'
  wsgiapp = make_app(config['global_conf'], **config)
  cls.app = paste.fixture.TestApp(wsgiapp)
 
  @classmethod
  def teardown_class(cls):
  config.clear()
  config.update(cls._original_config)
 
  import datetime
  from nose.tools import assert_equal
 
  from ckan.lib.create_test_data import CreateTestData
  from ckan import model
 
  from ckanext.dga_stats.stats import Stats, RevisionStats
  from ckanext.dga_stats.tests import StatsFixture
 
  class TestStatsPlugin(StatsFixture):
  @classmethod
  def setup_class(cls):
  super(TestStatsPlugin, cls).setup_class()
 
  CreateTestData.create_arbitrary([
  {'name':'test1', 'groups':['grp1'], 'tags':['tag1']},
  {'name':'test2', 'groups':['grp1', 'grp2'], 'tags':['tag1']},
  {'name':'test3', 'groups':['grp1', 'grp2'], 'tags':['tag1', 'tag2']},
  {'name':'test4'},
  ],
  extra_user_names=['bob'],
  admins=['bob'],
  )
  # hack revision timestamps to be this date
  week1 = datetime.datetime(2011, 1, 5)
  for rev in model.Session.query(model.Revision):
  rev.timestamp = week1 + datetime.timedelta(seconds=1)
 
  # week 2
  rev = model.repo.new_revision()
  rev.author = 'bob'
  rev.timestamp = datetime.datetime(2011, 1, 12)
  model.Package.by_name(u'test2').delete()
  model.repo.commit_and_remove()
 
  # week 3
  rev = model.repo.new_revision()
  rev.author = 'sandra'
  rev.timestamp = datetime.datetime(2011, 1, 19)
  model.Package.by_name(u'test3').title = 'Test 3'
  model.repo.commit_and_remove()
  rev = model.repo.new_revision()
  rev.author = 'sandra'
  rev.timestamp = datetime.datetime(2011, 1, 20)
  model.Package.by_name(u'test4').title = 'Test 4'
  model.repo.commit_and_remove()
 
  # week 4
  rev = model.repo.new_revision()
  rev.author = 'bob'
  rev.timestamp = datetime.datetime(2011, 1, 26)
  model.Package.by_name(u'test3').notes = 'Test 3 notes'
  model.repo.commit_and_remove()
 
  @classmethod
  def teardown_class(cls):
  CreateTestData.delete()
 
  def test_top_rated_packages(self):
  pkgs = Stats.top_rated_packages()
  assert pkgs == []
 
  def test_most_edited_packages(self):
  pkgs = Stats.most_edited_packages()
  pkgs = [(pkg.name, count) for pkg, count in pkgs]
  assert_equal(pkgs[0], ('test3', 3))
  assert_equal(pkgs[1][1], 2)
  assert_equal(pkgs[2][1], 2)
  assert_equal(pkgs[3], ('test1', 1))
 
  def test_largest_groups(self):
  grps = Stats.largest_groups()
  grps = [(grp.name, count) for grp, count in grps]
  assert_equal(grps, [('grp1', 3),
  ('grp2', 2)])
 
  def test_top_tags(self):
  tags = Stats.top_tags()
  tags = [(tag.name, count) for tag, count in tags]
  assert_equal(tags, [('tag1', 3),
  ('tag2', 1)])
 
  def test_top_package_owners(self):
  owners = Stats.top_package_owners()
  owners = [(owner.name, count) for owner, count in owners]
  assert_equal(owners, [('bob', 4)])
 
  def test_new_packages_by_week(self):
  new_packages_by_week = RevisionStats.get_by_week('new_packages')
  def get_results(week_number):
  date, ids, num, cumulative = new_packages_by_week[week_number]
  return (date, set([model.Session.query(model.Package).get(id).name for id in ids]), num, cumulative)
  assert_equal(get_results(0),
  ('2011-01-03', set((u'test1', u'test2', u'test3', u'test4')), 4, 4))
  assert_equal(get_results(1),
  ('2011-01-10', set([]), 0, 4))
  assert_equal(get_results(2),
  ('2011-01-17', set([]), 0, 4))
  assert_equal(get_results(3),
  ('2011-01-24', set([]), 0, 4))
 
  def test_deleted_packages_by_week(self):
  deleted_packages_by_week = RevisionStats.get_by_week('deleted_packages')
  def get_results(week_number):
  date, ids, num, cumulative = deleted_packages_by_week[week_number]
  return (date, [model.Session.query(model.Package).get(id).name for id in ids], num, cumulative)
  assert_equal(get_results(0),
  ('2011-01-10', [u'test2'], 1, 1))
  assert_equal(get_results(1),
  ('2011-01-17', [], 0, 1))
  assert_equal(get_results(2),
  ('2011-01-24', [], 0, 1))
  assert_equal(get_results(3),
  ('2011-01-31', [], 0, 1))
 
  def test_revisions_by_week(self):
  revisions_by_week = RevisionStats.get_by_week('package_revisions')
  def get_results(week_number):
  date, ids, num, cumulative = revisions_by_week[week_number]
  return (date, num, cumulative)
  num_setup_revs = revisions_by_week[0][2]
  assert 6 > num_setup_revs > 2, num_setup_revs
  assert_equal(get_results(0),
  ('2011-01-03', num_setup_revs, num_setup_revs))
  assert_equal(get_results(1),
  ('2011-01-10', 1, num_setup_revs+1))
  assert_equal(get_results(2),
  ('2011-01-17', 2, num_setup_revs+3))
  assert_equal(get_results(3),
  ('2011-01-24', 1, num_setup_revs+4))
 
  def test_num_packages_by_week(self):
  num_packages_by_week = RevisionStats.get_num_packages_by_week()
  # e.g. [('2011-05-30', 3, 3)]
  assert_equal(num_packages_by_week[0], ('2011-01-03', 4, 4))
  assert_equal(num_packages_by_week[1], ('2011-01-10', -1, 3))
  assert_equal(num_packages_by_week[2], ('2011-01-17', 0, 3))
  assert_equal(num_packages_by_week[3], ('2011-01-24', 0, 3))
 
  import os
 
  from ckan.tests import url_for
 
  from ckanext.dga_stats.tests import StatsFixture
 
  class TestStatsPlugin(StatsFixture):
 
  def test_01_config(self):
  from pylons import config
  paths = config['extra_public_paths']
  publicdir = os.path.join(os.path.dirname(os.path.dirname(__file__)),
  'public')
  assert paths.startswith(publicdir), (publicdir, paths)
 
  def test_02_index(self):
  url = url_for('stats')
  out = self.app.get(url)
  assert 'Total number of Datasets' in out, out
  assert 'Most Edited Datasets' in out, out
 
  def test_03_leaderboard(self):
  url = url_for('stats_action', action='leaderboard')
  out = self.app.get(url)
  assert 'Leaderboard' in out, out
 
 
file:a/setup.py -> file:b/setup.py
from setuptools import setup, find_packages from setuptools import setup, find_packages
   
version = '0.1' version = '0.1'
   
setup( setup(
name='ckanext-dga-stats', name='ckanext-dga-stats',
version=version, version=version,
description='Extension for customising CKAN Statistics for data.gov.au', description='Extension for customising CKAN Statistics for data.gov.au',
long_description='', long_description='',
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='', keywords='',
author='Alex Sadleir', author='Alex Sadleir',
author_email='alex.sadleir@linkdigital.com.au', author_email='alex.sadleir@linkdigital.com.au',
url='', url='',
license='', license='',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
namespace_packages=['ckanext', 'ckanext.dga-stats'], namespace_packages=['ckanext', 'ckanext.dga_stats'],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=[], install_requires=[],
entry_points=\ entry_points=\
""" """
[ckan.plugins] [ckan.plugins]
dga-stats=ckanext.dga-stats.plugin:StatsPlugin dga_stats=ckanext.dga_stats.plugin:StatsPlugin
""", """,
) )