Add summary and activity screens, remove private datasets from counts
Add summary and activity screens, remove private datasets from counts

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
 
  # this is a namespace package
  try:
  import pkg_resources
  pkg_resources.declare_namespace(__name__)
  except ImportError:
  import pkgutil
  __path__ = pkgutil.extend_path(__path__, __name__)
 
 Binary files /dev/null and b/ckanext/__init__.pyc differ
# 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
 
 Binary files /dev/null and b/ckanext/dga_stats/__init__.pyc 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.summary_stats = stats.summary_stats()
  c.activity_counts = stats.activity_counts()
  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')
 
 
 Binary files /dev/null and b/ckanext/dga_stats/controller.pyc differ
  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')
 
 Binary files /dev/null and b/ckanext/dga_stats/plugin.pyc 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);